2015-03-27 09:59:06 +09:00
/********************************************************************\
* gnc-datetime.cpp -- Date and Time classes for GnuCash *
* *
* Copyright 2015 John Ralls <jralls@ceridwen.us> *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
* *
\********************************************************************/
2017-10-26 11:14:21 +02:00
#include <config.h>
2015-03-27 09:59:06 +09:00
#include "platform.h"
#include <boost/date_time/gregorian/gregorian.hpp>
2015-12-15 13:38:50 -08:00
#include <boost/date_time/posix_time/posix_time.hpp>
2017-12-12 08:42:32 -08:00
#include <boost/date_time/local_time/local_time.hpp>
2019-01-19 13:41:17 -08:00
#include <boost/locale.hpp>
2017-04-21 16:58:20 +02:00
#include <boost/regex.hpp>
2025-12-28 15:42:06 -08:00
#include <stdexcept>
2024-09-02 00:21:35 +08:00
#include <unicode/smpdtfmt.h>
#include <unicode/locid.h>
#include <unicode/udat.h>
#include <unicode/parsepos.h>
#include <unicode/calendar.h>
2017-04-21 16:58:20 +02:00
#include <libintl.h>
2019-01-06 09:52:43 -08:00
#include <locale.h>
2025-12-28 15:42:06 -08:00
#include<chrono>
2017-04-21 16:58:20 +02:00
#include <map>
2015-03-27 09:59:06 +09:00
#include <memory>
2015-12-15 13:38:50 -08:00
#include <iostream>
#include <sstream>
2016-06-30 14:09:25 -07:00
#include <string>
2017-04-21 16:58:20 +02:00
#include <vector>
2024-03-12 21:32:16 +08:00
#include <optional>
#include <charconv>
2019-02-08 11:40:21 -08:00
#ifdef __MINGW32__
#include <codecvt>
#endif
2019-01-06 09:52:43 -08:00
#include <gnc-locale-utils.hpp>
2015-03-27 09:59:06 +09:00
#include "gnc-timezone.hpp"
#include "gnc-datetime.hpp"
2017-04-21 16:58:20 +02:00
#define N_(string) string //So that xgettext will find it
2021-03-20 15:49:54 -07:00
using PTZ = boost :: local_time :: posix_time_zone ;
2015-03-27 09:59:06 +09:00
using Date = boost :: gregorian :: date ;
using Month = boost :: gregorian :: greg_month ;
using PTime = boost :: posix_time :: ptime ;
using LDT = boost :: local_time :: local_date_time ;
using Duration = boost :: posix_time :: time_duration ;
using LDTBase = boost :: local_time :: local_date_time_base < PTime , boost :: date_time :: time_zone_base < PTime , char >> ;
2015-12-26 12:18:27 -08:00
using boost :: date_time :: not_a_date_time ;
2015-03-27 09:59:06 +09:00
using time64 = int64_t ;
2018-11-02 10:29:52 -07:00
static const TimeZoneProvider ltzp ;
static const TimeZoneProvider * tzp = & ltzp ;
2015-03-27 09:59:06 +09:00
// For converting to/from POSIX time.
static const PTime unix_epoch ( Date ( 1970 , boost :: gregorian :: Jan , 1 ),
2015-04-28 17:53:35 -07:00
boost :: posix_time :: seconds ( 0 ));
2015-04-28 10:06:18 -07:00
static const TZ_Ptr utc_zone ( new boost :: local_time :: posix_time_zone ( "UTC-0" ));
2015-03-27 09:59:06 +09:00
2018-11-02 10:29:52 -07:00
/* Backdoor to enable unittests to temporarily override the timezone: */
void _set_tzp ( TimeZoneProvider & tz );
void _reset_tzp ();
2024-09-02 00:21:35 +08:00
static Date gregorian_date_from_locale_string ( const std :: string & str );
2015-04-07 15:36:40 -07:00
/* 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. */
#ifndef BOOST_DATE_TIME_HAS_NANOSECONDS
static constexpr auto ticks_per_second = INT64_C ( 1000000 );
#else
static constexpr auto ticks_per_second = INT64_C ( 1000000000 );
#endif
2015-03-27 09:59:06 +09:00
2017-04-21 16:58:20 +02:00
/* Vector of date formats understood by gnucash and corresponding regex
2024-09-02 00:21:35 +08:00
* and/or string->gregorian_date to parse each from an external source
2017-04-21 16:58:20 +02:00
* Note: while the format names are using a "-" as separator, the
* regexes will accept any of "-/.' " and will also work for dates
* without separators.
*/
const std :: vector < GncDateFormat > GncDate :: c_formats ({
GncDateFormat {
N_ ( "y-m-d" ),
2024-09-02 00:21:35 +08:00
boost :: gregorian :: from_string ,
2017-04-21 16:58:20 +02:00
"(?:" // either y-m-d
"(?<YEAR>[0-9]+)[-/.' ]+"
"(?<MONTH>[0-9]+)[-/.' ]+"
"(?<DAY>[0-9]+)"
"|" // or CCYYMMDD
"(?<YEAR>[0-9]{4})"
"(?<MONTH>[0-9]{2})"
"(?<DAY>[0-9]{2})"
")"
},
GncDateFormat {
N_ ( "d-m-y" ),
2024-09-02 00:21:35 +08:00
boost :: gregorian :: from_uk_string ,
2017-04-21 16:58:20 +02:00
"(?:" // either d-m-y
"(?<DAY>[0-9]+)[-/.' ]+"
"(?<MONTH>[0-9]+)[-/.' ]+"
"(?<YEAR>[0-9]+)"
"|" // or DDMMCCYY
"(?<DAY>[0-9]{2})"
"(?<MONTH>[0-9]{2})"
"(?<YEAR>[0-9]{4})"
")"
},
GncDateFormat {
N_ ( "m-d-y" ),
2024-09-02 00:21:35 +08:00
boost :: gregorian :: from_us_string ,
2017-04-21 16:58:20 +02:00
"(?:" // either m-d-y
"(?<MONTH>[0-9]+)[-/.' ]+"
"(?<DAY>[0-9]+)[-/.' ]+"
"(?<YEAR>[0-9]+)"
"|" // or MMDDCCYY
"(?<MONTH>[0-9]{2})"
"(?<DAY>[0-9]{2})"
"(?<YEAR>[0-9]{4})"
")"
},
// Note year is still checked for in the regexes below
// This is to be able to raise an error if one is found for a yearless date format
GncDateFormat {
( N_ ( "d-m" )),
"(?:" // either d-m(-y)
"(?<DAY>[0-9]+)[-/.' ]+"
"(?<MONTH>[0-9]+)(?:[-/.' ]+"
"(?<YEAR>[0-9]+))?"
"|" // or DDMM(CCYY)
"(?<DAY>[0-9]{2})"
"(?<MONTH>[0-9]{2})"
"(?<YEAR>[0-9]+)?"
")"
},
GncDateFormat {
( N_ ( "m-d" )),
"(?:" // either m-d(-y)
"(?<MONTH>[0-9]+)[-/.' ]+"
"(?<DAY>[0-9]+)(?:[-/.' ]+"
"(?<YEAR>[0-9]+))?"
"|" // or MMDD(CCYY)
"(?<MONTH>[0-9]{2})"
"(?<DAY>[0-9]{2})"
"(?<YEAR>[0-9]+)?"
")"
2024-09-02 00:21:35 +08:00
},
GncDateFormat { N_ ( "Locale" ), gregorian_date_from_locale_string },
2017-04-21 16:58:20 +02:00
});
2015-03-27 09:59:06 +09:00
/** Private implementation of GncDateTime. See the documentation for that class.
*/
static LDT
LDT_from_unix_local ( const time64 time )
{
2015-04-28 15:23:26 -07:00
try
{
2015-04-28 17:53:35 -07:00
PTime temp ( unix_epoch . date (),
boost :: posix_time :: hours ( time / 3600 ) +
boost :: posix_time :: seconds ( time % 3600 ));
2018-11-02 10:29:52 -07:00
auto tz = tzp -> get ( temp . date (). year ());
2015-04-28 17:53:35 -07:00
return LDT ( temp , tz );
2015-04-28 15:23:26 -07:00
}
2018-04-03 22:02:44 -04:00
catch ( boost :: gregorian :: bad_year & )
2015-04-28 15:23:26 -07:00
{
2015-04-28 17:53:35 -07:00
throw ( std :: invalid_argument ( "Time value is outside the supported year range." ));
2015-04-28 15:23:26 -07:00
}
2025-12-28 15:42:06 -08:00
catch ( std :: out_of_range & )
{
throw ( std :: invalid_argument ( "Time value is outside the supported year range." ));
}
2015-03-27 09:59:06 +09:00
}
2021-03-20 15:49:54 -07:00
/* If a date-time falls in a DST transition the LDT constructor will
* fail because either the date-time doesn't exist (when starting DST
* because the transition skips an hour) or is ambiguous (when ending
* because the transition hour is repeated). We try again an hour
* later to be outside the DST transition. When starting DST that's
* now the correct time but at the end of DST we need to set the
* returned time back an hour.
*/
static LDT
LDT_with_pushup ( const Date & tdate , const Duration & tdur , const TZ_Ptr tz ,
bool putback )
{
static const boost :: posix_time :: hours pushup { 1 };
LDT ldt { tdate , tdur + pushup , tz , LDTBase :: NOT_DATE_TIME_ON_ERROR };
if ( ldt . is_special ())
{
std :: string error { "Couldn't create a valid datetime at " };
error += to_simple_string ( tdate ) + " " ;
error += to_simple_string ( tdur ) + " TZ " ;
error += tz -> std_zone_abbrev ();
throw ( std :: invalid_argument { error });
}
if ( putback )
ldt -= pushup ;
return ldt ;
}
2015-03-27 09:59:06 +09:00
2015-04-26 16:44:39 -07:00
static LDT
2021-03-20 15:49:54 -07:00
LDT_from_date_time ( const Date & tdate , const Duration & tdur , const TZ_Ptr tz )
2015-04-26 16:44:39 -07:00
{
2018-05-25 09:18:06 -07:00
2015-04-28 15:23:26 -07:00
try
{
2017-12-12 08:42:32 -08:00
LDT ldt ( tdate , tdur , tz , LDTBase :: EXCEPTION_ON_ERROR );
return ldt ;
2015-04-28 15:23:26 -07:00
}
2021-03-20 15:49:54 -07:00
catch ( const boost :: local_time :: time_label_invalid & err )
2015-04-28 15:23:26 -07:00
{
2021-03-20 15:49:54 -07:00
return LDT_with_pushup ( tdate , tdur , tz , false );
2015-04-28 15:23:26 -07:00
}
2021-03-20 15:49:54 -07:00
catch ( const boost :: local_time :: ambiguous_result & err )
2017-12-12 08:42:32 -08:00
{
2021-03-20 15:49:54 -07:00
return LDT_with_pushup ( tdate , tdur , tz , true );
2017-12-12 08:42:32 -08:00
}
2021-03-20 15:49:54 -07:00
catch ( boost :: gregorian :: bad_year & )
2017-12-12 08:42:32 -08:00
{
2021-03-20 15:49:54 -07:00
throw ( std :: invalid_argument ( "Time value is outside the supported year range." ));
2017-12-12 08:42:32 -08:00
}
2025-12-28 15:42:06 -08:00
catch ( std :: out_of_range & )
{
throw ( std :: invalid_argument ( "Time value is outside the supported year range." ));
}
2021-03-20 15:49:54 -07:00
2015-04-26 16:44:39 -07:00
}
2021-03-20 15:49:54 -07:00
static LDT
LDT_from_date_daypart ( const Date & date , DayPart part , const TZ_Ptr tz )
{
using hours = boost :: posix_time :: hours ;
static const Duration day_begin { 0 , 0 , 0 };
static const Duration day_neutral { 10 , 59 , 0 };
static const Duration day_end { 23 , 59 , 59 };
switch ( part )
{
case DayPart :: start :
return LDT_from_date_time ( date , day_begin , tz );
case DayPart :: end :
return LDT_from_date_time ( date , day_end , tz );
default : // To stop gcc from emitting a control reaches end of non-void function.
case DayPart :: neutral :
PTime pt { date , day_neutral };
LDT lt { pt , tz };
auto offset = lt . local_time () - lt . utc_time ();
if ( offset < hours ( - 10 ))
lt -= hours ( offset . hours () + 10 );
if ( offset > hours ( 13 ))
lt += hours ( 13 - offset . hours ());
return lt ;
}
}
static LDT
LDT_from_struct_tm ( const struct tm tm )
{
2021-06-08 13:14:30 -07:00
try
{
Date tdate { boost :: gregorian :: date_from_tm ( tm )};
Duration tdur { boost :: posix_time :: time_duration ( tm . tm_hour , tm . tm_min ,
tm . tm_sec , 0 )};
TZ_Ptr tz { tzp -> get ( tdate . year ())};
return LDT_from_date_time ( tdate , tdur , tz );
}
catch ( const boost :: gregorian :: bad_year & )
{
throw ( std :: invalid_argument { "Time value is outside the supported year range." });
}
2025-12-28 15:42:06 -08:00
catch ( std :: out_of_range & )
{
throw ( std :: invalid_argument ( "Time value is outside the supported year range." ));
}
2021-03-20 15:49:54 -07:00
}
2017-12-12 21:01:58 -08:00
2018-11-02 10:29:52 -07:00
void
_set_tzp ( TimeZoneProvider & new_tzp )
{
tzp = & new_tzp ;
}
void
_reset_tzp ()
{
tzp = & ltzp ;
}
2015-04-07 15:36:40 -07:00
class GncDateTimeImpl
{
public :
2025-12-28 15:42:06 -08:00
/* Boost::date_time's localtime function relies on the C library's, so it may
* suffer from the 2038 failure. std::chrono is supposed to be immune to that.
*/
GncDateTimeImpl () : m_time ( LDT_from_unix_local ( std :: chrono :: duration_cast < std :: chrono :: seconds > ( std :: chrono :: system_clock :: now (). time_since_epoch ()). count ())) {}
2015-04-07 15:36:40 -07:00
GncDateTimeImpl ( const time64 time ) : m_time ( LDT_from_unix_local ( time )) {}
2015-04-26 16:44:39 -07:00
GncDateTimeImpl ( const struct tm tm ) : m_time ( LDT_from_struct_tm ( tm )) {}
2017-04-20 22:58:35 +02:00
GncDateTimeImpl ( const GncDateImpl & date , DayPart part = DayPart :: neutral );
2024-03-12 21:32:16 +08:00
GncDateTimeImpl ( const std :: string & str ) : GncDateTimeImpl ( str . c_str ()) {};
GncDateTimeImpl ( const char * str );
2018-11-02 10:29:52 -07:00
GncDateTimeImpl ( PTime && pt ) : m_time ( pt , tzp -> get ( pt . date (). year ())) {}
2015-04-07 15:36:40 -07:00
GncDateTimeImpl ( LDT && ldt ) : m_time ( ldt ) {}
2015-04-10 09:33:51 -07:00
operator time64 () const ;
2015-04-26 16:44:39 -07:00
operator struct tm () const ;
2018-11-02 10:29:52 -07:00
void now () { m_time = boost :: local_time :: local_sec_clock :: local_time ( tzp -> get ( boost :: gregorian :: day_clock :: local_day (). year ())); }
2015-04-26 16:44:39 -07:00
long offset () const ;
2015-04-28 15:23:26 -07:00
struct tm utc_tm () const { return to_tm ( m_time . utc_time ()); }
2015-04-28 10:06:18 -07:00
std :: unique_ptr < GncDateImpl > date () const ;
2015-04-26 18:01:23 -07:00
std :: string format ( const char * format ) const ;
2016-11-06 09:39:21 -08:00
std :: string format_zulu ( const char * format ) const ;
2018-12-28 16:39:02 -08:00
std :: string format_iso8601 () const ;
2019-01-05 14:53:25 -08:00
static std :: string timestamp ();
2015-04-07 15:36:40 -07:00
private :
LDT m_time ;
};
2015-04-10 09:33:51 -07:00
2017-04-20 21:42:24 +02:00
/** Private implementation of GncDate. See the documentation for that class.
*/
class GncDateImpl
{
public :
GncDateImpl () : m_greg ( boost :: gregorian :: day_clock :: local_day ()) {}
GncDateImpl ( const int year , const int month , const int day ) :
m_greg ( year , static_cast < Month > ( month ), day ) {}
GncDateImpl ( Date d ) : m_greg ( d ) {}
2017-04-21 16:58:20 +02:00
GncDateImpl ( const std :: string str , const std :: string fmt );
2017-04-20 21:42:24 +02:00
void today () { m_greg = boost :: gregorian :: day_clock :: local_day (); }
2023-01-09 14:36:06 -08:00
gnc_ymd year_month_day () const ;
2017-04-20 21:42:24 +02:00
std :: string format ( const char * format ) const ;
2019-02-08 11:40:21 -08:00
std :: string format_zulu ( const char * format ) const {
return this -> format ( format );
}
2017-04-20 21:42:24 +02:00
private :
Date m_greg ;
2017-04-20 22:58:35 +02:00
friend GncDateTimeImpl :: GncDateTimeImpl ( const GncDateImpl & , DayPart );
2017-04-29 18:01:12 +02:00
friend bool operator < ( const GncDateImpl & , const GncDateImpl & );
friend bool operator > ( const GncDateImpl & , const GncDateImpl & );
friend bool operator == ( const GncDateImpl & , const GncDateImpl & );
friend bool operator <= ( const GncDateImpl & , const GncDateImpl & );
friend bool operator >= ( const GncDateImpl & , const GncDateImpl & );
friend bool operator != ( const GncDateImpl & , const GncDateImpl & );
2017-04-20 21:42:24 +02:00
};
2021-03-20 15:49:54 -07:00
/* Needs to be separately defined so that the friend decl can grant
* access to date.m_greg.
2017-04-20 21:42:24 +02:00
*/
2017-04-20 22:58:35 +02:00
GncDateTimeImpl :: GncDateTimeImpl ( const GncDateImpl & date , DayPart part ) :
2021-03-20 15:49:54 -07:00
m_time { LDT_from_date_daypart ( date . m_greg , part ,
tzp -> get ( date . m_greg . year ()))} {}
2017-04-20 22:58:35 +02:00
2021-03-20 15:49:54 -07:00
/* Member function definitions for GncDateTimeImpl.
*/
2018-01-09 13:58:25 -08:00
2024-03-12 21:32:16 +08:00
static bool
parse_chars_into_num ( const char * ptr , const char * end_ptr , int32_t & rv ) noexcept
{
auto result = std :: from_chars ( ptr , end_ptr , rv );
return ( result . ec == std :: errc () && result . ptr == end_ptr );
}
static std :: optional < PTime >
fast_iso8601_utc_parse ( const char * str )
{
int32_t year , month , mday , hour , min , sec ;
2026-03-15 13:39:09 +08:00
const size_t len = str ? strnlen ( str , 26 ) : 0 ;
2024-03-12 21:32:16 +08:00
// parse iso-8601 utc format "YYYY-MM-DD HH:MM:SS +0000"
2026-03-15 13:39:09 +08:00
constexpr size_t expanded_iso_string_len = 25 ;
if ( len == expanded_iso_string_len &&
parse_chars_into_num ( str , str + 4 , year ) && str [ 4 ] == '-' &&
2024-03-12 21:32:16 +08:00
parse_chars_into_num ( str + 5 , str + 7 , month ) && str [ 7 ] == '-' &&
parse_chars_into_num ( str + 8 , str + 10 , mday ) && str [ 10 ] == ' ' &&
parse_chars_into_num ( str + 11 , str + 13 , hour ) && str [ 13 ] == ':' &&
parse_chars_into_num ( str + 14 , str + 16 , min ) && str [ 16 ] == ':' &&
parse_chars_into_num ( str + 17 , str + 19 , sec ) && str [ 19 ] == ' ' &&
! strcmp ( str + 20 , "+0000" ))
{
return PTime ( boost :: gregorian :: date ( year , month , mday ),
boost :: posix_time :: time_duration ( hour , min , sec ));
}
// parse compressed iso-8601 format "YYYYMMDDHHMMSS"
2026-03-15 13:39:09 +08:00
constexpr size_t compact_iso_string_len = 14 ;
if ( len == compact_iso_string_len &&
parse_chars_into_num ( str , str + 4 , year ) &&
parse_chars_into_num ( str + 4 , str + 6 , month ) &&
2024-03-12 21:32:16 +08:00
parse_chars_into_num ( str + 6 , str + 8 , mday ) &&
parse_chars_into_num ( str + 8 , str + 10 , hour ) &&
parse_chars_into_num ( str + 10 , str + 12 , min ) &&
2026-03-15 13:39:09 +08:00
parse_chars_into_num ( str + 12 , str + 14 , sec ))
2024-03-12 21:32:16 +08:00
{
return PTime ( boost :: gregorian :: date ( year , month , mday ),
boost :: posix_time :: time_duration ( hour , min , sec ));
}
return {};
}
2018-01-09 13:58:25 -08:00
static TZ_Ptr
tz_from_string ( std :: string str )
{
if ( str . empty ()) return utc_zone ;
std :: string tzstr = "XXX" + str ;
if ( tzstr . length () > 6 && tzstr [ 6 ] != ':' ) //6 for XXXsHH, s is + or -
tzstr . insert ( 6 , ":" );
if ( tzstr . length () > 9 && tzstr [ 9 ] != ':' ) //9 for XXXsHH:MM
{
tzstr . insert ( 9 , ":" );
}
return TZ_Ptr ( new PTZ ( tzstr ));
}
2024-03-12 21:32:16 +08:00
GncDateTimeImpl :: GncDateTimeImpl ( const char * str ) :
2015-04-28 10:06:18 -07:00
m_time ( unix_epoch , utc_zone )
{
2024-03-12 21:32:16 +08:00
if ( ! str || ! str [ 0 ]) return ;
2018-04-08 17:37:39 -07:00
TZ_Ptr tzptr ;
2015-04-28 17:53:35 -07:00
try
{
2024-03-12 21:32:16 +08:00
if ( auto res = fast_iso8601_utc_parse ( str ))
{
m_time = LDT_from_date_time ( res -> date (), res -> time_of_day (), utc_zone );
return ;
}
2018-04-08 17:37:39 -07:00
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})?)?$" );
static const boost :: regex non_delim ( "^( \\ d{14}(?: \\ . \\ d{0,9})?) \\ s*([+-] \\ d{2} \\ s*(:? \\ d{2})?)?$" );
PTime pdt ;
2024-03-12 21:32:16 +08:00
boost :: cmatch sm ;
2018-04-08 17:37:39 -07:00
if ( regex_match ( str , sm , non_delim ))
{
std :: string time_str ( sm [ 1 ]);
time_str . insert ( 8 , "T" );
pdt = boost :: posix_time :: from_iso_string ( time_str );
}
else if ( regex_match ( str , sm , delim_iso ))
{
pdt = boost :: posix_time :: time_from_string ( sm [ 1 ]);
}
else
{
2024-03-12 21:32:16 +08:00
throw ( std :: invalid_argument ( "The date string was not formatted in a way that GncDateTime(const char*) knows how to parse." ));
2018-04-08 17:37:39 -07:00
}
std :: string tzstr ( "" );
if ( sm [ 2 ]. matched )
tzstr += sm [ 2 ];
tzptr = tz_from_string ( tzstr );
2021-03-20 15:49:54 -07:00
m_time = LDT_from_date_time ( pdt . date (), pdt . time_of_day (), tzptr );
2015-04-28 17:53:35 -07:00
}
2018-04-03 22:02:44 -04:00
catch ( boost :: gregorian :: bad_year & )
2015-04-28 17:53:35 -07:00
{
throw ( std :: invalid_argument ( "The date string was outside of the supported year range." ));
2015-04-28 10:06:18 -07:00
}
2025-12-28 15:42:06 -08:00
catch ( std :: out_of_range & )
{
throw ( std :: invalid_argument ( "The date string was outside of the supported year range." ));
}
2018-01-09 13:58:25 -08:00
/* Bug 767824: A GLib bug in parsing the UTC timezone on Windows may have
* created a bogus timezone of a random number of minutes. Since there are
* no fractional-hour timezones around the prime meridian we can safely
* check for this in files by resetting to UTC if there's a
* less-than-an-hour offset.
*/
auto offset = tzptr -> base_utc_offset (). seconds ();
if ( offset != 0 && std :: abs ( offset ) < 3600 )
m_time = m_time . local_time_in ( utc_zone );
2015-04-28 10:06:18 -07:00
}
2015-04-10 09:33:51 -07:00
GncDateTimeImpl :: operator time64 () const
{
auto duration = m_time . utc_time () - unix_epoch ;
auto secs = duration . ticks ();
secs /= ticks_per_second ;
return secs ;
2015-04-07 15:36:40 -07:00
}
2015-04-26 16:44:39 -07:00
GncDateTimeImpl :: operator struct tm () const
{
2015-04-28 15:23:26 -07:00
struct tm time = to_tm ( m_time );
#if HAVE_STRUCT_TM_GMTOFF
time . tm_gmtoff = offset ();
#endif
return time ;
2015-04-26 16:44:39 -07:00
}
long
GncDateTimeImpl :: offset () const
{
auto offset = m_time . local_time () - m_time . utc_time ();
return offset . total_seconds ();
}
2015-04-28 10:06:18 -07:00
std :: unique_ptr < GncDateImpl >
GncDateTimeImpl :: date () const
{
return std :: unique_ptr < GncDateImpl > ( new GncDateImpl ( m_time . local_time (). date ()));
}
2018-05-28 08:56:01 -07:00
/* The 'O', 'E', and '-' format modifiers are not supported by
* boost's output facets. Remove them.
*/
static inline std :: string
normalize_format ( const std :: string & format )
{
bool is_pct = false ;
std :: string normalized ;
std :: remove_copy_if (
format . begin (), format . end (), back_inserter ( normalized ),
[ & is_pct ]( char e ){
bool r = ( is_pct && ( e == 'E' || e == 'O' || e == '-' ));
is_pct = e == '%' ;
return r ;
});
return normalized ;
}
2019-02-08 11:40:21 -08:00
#ifdef __MINGW32__
constexpr size_t DATEBUFLEN = 100 ;
static std :: string
win_date_format ( std :: string format , struct tm tm )
{
2026-01-01 17:41:08 -08:00
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
2019-02-08 11:40:21 -08:00
wchar_t buf [ DATEBUFLEN ];
2026-03-29 13:46:27 -07:00
memset ( buf , 0 , DATEBUFLEN * sizeof ( wchar_t ));
2019-02-08 11:40:21 -08:00
std :: wstring_convert < std :: codecvt_utf8_utf16 < wchar_t > , wchar_t > conv ;
2026-03-29 13:46:27 -07:00
[[maybe_unused]] auto numchars = wcsftime ( buf , DATEBUFLEN - 1 , conv . from_bytes ( format ). c_str (), & tm );
2019-02-08 11:40:21 -08:00
return conv . to_bytes ( buf );
2026-01-01 17:41:08 -08:00
#pragma GCC diagnostic pop
2019-02-08 11:40:21 -08:00
}
/* Microsoft's strftime uses the time zone flags differently from
* boost::date_time so we need to handle any before passing the
* format string to strftime.
*/
inline std :: string
win_format_tz_abbrev ( std :: string format , TZ_Ptr tz , bool is_dst )
{
size_t pos = format . find ( "%z" );
if ( pos != std :: string :: npos )
{
auto tzabbr = tz -> has_dst () && is_dst ? tz -> dst_zone_abbrev () :
tz -> std_zone_abbrev ();
format . replace ( pos , 2 , tzabbr );
}
return format ;
}
inline std :: string
win_format_tz_name ( std :: string format , TZ_Ptr tz , bool is_dst )
{
size_t pos = format . find ( "%Z" );
if ( pos != std :: string :: npos )
{
auto tzname = tz -> has_dst () && is_dst ? tz -> dst_zone_name () :
tz -> std_zone_name ();
format . replace ( pos , 2 , tzname );
}
return format ;
}
2018-05-28 08:56:01 -07:00
2019-02-08 11:40:21 -08:00
inline std :: string
win_format_tz_posix ( std :: string format , TZ_Ptr tz )
{
size_t pos = format . find ( "%ZP" );
if ( pos != std :: string :: npos )
format . replace ( pos , 2 , tz -> to_posix_string ());
return format ;
}
#endif
2015-04-26 18:01:23 -07:00
std :: string
GncDateTimeImpl :: format ( const char * format ) const
{
2019-02-08 11:40:21 -08:00
#ifdef __MINGW32__
auto tz = m_time . zone ();
auto tm = static_cast < struct tm > ( * this );
auto sformat = win_format_tz_abbrev ( format , tz , tm . tm_isdst );
sformat = win_format_tz_name ( sformat , tz , tm . tm_isdst );
sformat = win_format_tz_posix ( sformat , tz );
return win_date_format ( sformat , tm );
#else
using Facet = boost :: local_time :: local_time_facet ;
auto output_facet ( new Facet ( normalize_format ( format ). c_str ()));
2015-04-26 18:01:23 -07:00
std :: stringstream ss ;
2019-02-08 11:40:21 -08:00
ss . imbue ( std :: locale ( gnc_get_locale (), output_facet ));
ss << m_time ;
2015-04-26 18:01:23 -07:00
return ss . str ();
2019-02-08 11:40:21 -08:00
#endif
2015-04-26 18:01:23 -07:00
}
2016-11-06 09:39:21 -08:00
std :: string
GncDateTimeImpl :: format_zulu ( const char * format ) const
{
2019-02-08 11:40:21 -08:00
#ifdef __MINGW32__
2025-09-04 15:21:32 -07:00
auto tz = utc_zone ;
2019-02-08 11:40:21 -08:00
auto tm = static_cast < struct tm > ( * this );
auto sformat = win_format_tz_abbrev ( format , tz , tm . tm_isdst );
sformat = win_format_tz_name ( sformat , tz , tm . tm_isdst );
sformat = win_format_tz_posix ( sformat , tz );
return win_date_format ( sformat , utc_tm ());
#else
using Facet = boost :: local_time :: local_time_facet ;
2021-03-21 13:53:48 -07:00
auto zulu_time = LDT { m_time . utc_time (), utc_zone };
2019-02-08 11:40:21 -08:00
auto output_facet ( new Facet ( normalize_format ( format ). c_str ()));
2016-11-06 09:39:21 -08:00
std :: stringstream ss ;
2019-02-08 11:40:21 -08:00
ss . imbue ( std :: locale ( gnc_get_locale (), output_facet ));
ss << zulu_time ;
2016-11-06 09:39:21 -08:00
return ss . str ();
2019-02-08 11:40:21 -08:00
#endif
2016-11-06 09:39:21 -08:00
}
2018-12-28 16:39:02 -08:00
std :: string
GncDateTimeImpl :: format_iso8601 () const
{
auto str = boost :: posix_time :: to_iso_extended_string ( m_time . utc_time ());
str [ 10 ] = ' ' ;
return str . substr ( 0 , 19 );
}
2019-01-05 14:53:25 -08:00
std :: string
GncDateTimeImpl :: timestamp ()
{
GncDateTimeImpl gdt ;
auto str = boost :: posix_time :: to_iso_string ( gdt . m_time . local_time ());
return str . substr ( 0 , 8 ) + str . substr ( 9 , 15 );
}
2024-09-02 00:21:35 +08:00
struct ICUResources
{
std :: unique_ptr < icu :: DateFormat > formatter ;
std :: unique_ptr < icu :: Calendar > calendar ;
};
static ICUResources &
get_icu_resources ()
{
static ICUResources rv ;
if ( ! rv . formatter )
{
icu :: Locale locale ;
if ( auto lc_time_locale = setlocale ( LC_TIME , nullptr ))
{
std :: string localeStr ( lc_time_locale );
if ( size_t dotPos = localeStr . find ( '.' ); dotPos != std :: string :: npos )
localeStr = localeStr . substr ( 0 , dotPos );
locale = icu :: Locale :: createCanonical ( localeStr . c_str ());
}
rv . formatter . reset ( icu :: DateFormat :: createDateInstance ( icu :: DateFormat :: kDefault , locale ));
if ( ! rv . formatter )
throw std :: invalid_argument ( "Cannot create date formatter." );
UErrorCode status = U_ZERO_ERROR ;
rv . calendar . reset ( icu :: Calendar :: createInstance ( locale , status ));
if ( U_FAILURE ( status ))
throw std :: invalid_argument ( "Cannot create calendar instance." );
rv . calendar -> setLenient ( false );
}
return rv ;
}
static Date
gregorian_date_from_locale_string ( const std :: string & str )
{
ICUResources & resources = get_icu_resources ();
icu :: UnicodeString input = icu :: UnicodeString :: fromUTF8 ( str );
icu :: ParsePosition parsePos ;
UDate date = resources . formatter -> parse ( input , parsePos );
if ( parsePos . getErrorIndex () != - 1 || parsePos . getIndex () != input . length ())
throw std :: invalid_argument ( "Cannot parse string" );
UErrorCode status = U_ZERO_ERROR ;
resources . calendar -> setTime ( date , status );
if ( U_FAILURE ( status ))
throw std :: invalid_argument ( "Cannot set calendar time" );
return Date ( resources . calendar -> get ( UCAL_YEAR , status ),
resources . calendar -> get ( UCAL_MONTH , status ) + 1 ,
resources . calendar -> get ( UCAL_DATE , status ));
}
2017-04-21 16:58:20 +02:00
/* Member function definitions for GncDateImpl.
2017-04-20 21:42:24 +02:00
*/
2017-04-21 16:58:20 +02:00
GncDateImpl :: GncDateImpl ( const std :: string str , const std :: string fmt ) :
m_greg ( boost :: gregorian :: day_clock :: local_day ()) /* Temporarily initialized to today, will be used and adjusted in the code below */
{
auto iter = std :: find_if ( GncDate :: c_formats . cbegin (), GncDate :: c_formats . cend (),
[ & fmt ]( const GncDateFormat & v ){ return ( v . m_fmt == fmt ); } );
if ( iter == GncDate :: c_formats . cend ())
throw std :: invalid_argument ( N_ ( "Unknown date format specifier passed as argument." ));
2024-09-02 00:21:35 +08:00
if ( iter -> m_str_to_date )
{
try
{
m_greg = ( * iter -> m_str_to_date )( str );
return ;
}
catch (...) {} // with any string->date exception, try regex
}
if ( iter -> m_re . empty ())
throw std :: invalid_argument ( "No regex pattern available" );
2017-04-21 16:58:20 +02:00
boost :: smatch what ;
2026-05-27 22:41:20 +08:00
if ( ! boost :: regex_search ( str , what , iter -> m_re )) // regex didn't find a match
2017-04-21 16:58:20 +02:00
throw std :: invalid_argument ( N_ ( "Value can't be parsed into a date using the selected date format." ));
// Bail out if a year was found with a yearless format specifier
auto fmt_has_year = ( fmt . find ( 'y' ) != std :: string :: npos );
if ( ! fmt_has_year && ( what . length ( "YEAR" ) != 0 ))
throw std :: invalid_argument ( N_ ( "Value appears to contain a year while the selected format forbids this." ));
int year ;
if ( fmt_has_year )
{
/* The input dates have a year, so use that one */
year = std :: stoi ( what . str ( "YEAR" ));
/* We assume two-digit years to be in the range 1969 - 2068. */
if ( year < 69 )
year += 2000 ;
else if ( year < 100 )
year += 1900 ;
}
else /* The input dates have no year, so use current year */
year = m_greg . year (); // Can use m_greg here as it was already initialized in the initializer list earlier
m_greg = Date ( year ,
static_cast < Month > ( std :: stoi ( what . str ( "MONTH" ))),
std :: stoi ( what . str ( "DAY" )));
}
2023-01-09 14:36:06 -08:00
gnc_ymd
2017-04-20 21:42:24 +02:00
GncDateImpl :: year_month_day () const
2015-04-07 15:36:40 -07:00
{
2017-04-20 21:42:24 +02:00
auto boost_ymd = m_greg . year_month_day ();
return { boost_ymd . year , boost_ymd . month . as_number (), boost_ymd . day };
2015-04-07 15:36:40 -07:00
}
2015-05-04 15:03:54 -07:00
std :: string
2017-04-20 21:42:24 +02:00
GncDateImpl :: format ( const char * format ) const
2015-04-28 10:06:18 -07:00
{
2019-02-08 11:40:21 -08:00
#ifdef __MINGW32__
return win_date_format ( format , to_tm ( m_greg ));
#else
2017-04-20 21:42:24 +02:00
using Facet = boost :: gregorian :: date_facet ;
std :: stringstream ss ;
//The stream destructor frees the facet, so it must be heap-allocated.
2018-05-28 08:56:01 -07:00
auto output_facet ( new Facet ( normalize_format ( format ). c_str ()));
2019-01-06 09:52:43 -08:00
ss . imbue ( std :: locale ( gnc_get_locale (), output_facet ));
2017-04-20 21:42:24 +02:00
ss << m_greg ;
return ss . str ();
2019-02-08 11:40:21 -08:00
#endif
2015-04-28 10:06:18 -07:00
}
2017-04-29 18:01:12 +02:00
bool operator < ( const GncDateImpl & a , const GncDateImpl & b ) { return a . m_greg < b . m_greg ; }
bool operator > ( const GncDateImpl & a , const GncDateImpl & b ) { return a . m_greg > b . m_greg ; }
bool operator == ( const GncDateImpl & a , const GncDateImpl & b ) { return a . m_greg == b . m_greg ; }
bool operator <= ( const GncDateImpl & a , const GncDateImpl & b ) { return a . m_greg <= b . m_greg ; }
bool operator >= ( const GncDateImpl & a , const GncDateImpl & b ) { return a . m_greg >= b . m_greg ; }
bool operator != ( const GncDateImpl & a , const GncDateImpl & b ) { return a . m_greg != b . m_greg ; }
2017-04-20 21:42:24 +02:00
/* =================== Presentation-class Implementations ====================*/
2015-05-04 15:03:54 -07:00
/* GncDateTime */
2015-03-27 09:59:06 +09:00
GncDateTime :: GncDateTime () : m_impl ( new GncDateTimeImpl ) {}
2015-04-26 16:44:39 -07:00
GncDateTime :: GncDateTime ( const time64 time ) :
m_impl ( new GncDateTimeImpl ( time )) {}
GncDateTime :: GncDateTime ( const struct tm tm ) :
m_impl ( new GncDateTimeImpl ( tm )) {}
2024-03-12 21:32:16 +08:00
GncDateTime :: GncDateTime ( const std :: string & str ) :
m_impl ( new GncDateTimeImpl ( str )) {}
GncDateTime :: GncDateTime ( const char * str ) :
2015-04-28 10:06:18 -07:00
m_impl ( new GncDateTimeImpl ( str )) {}
2015-03-27 09:59:06 +09:00
GncDateTime ::~ GncDateTime () = default ;
2015-04-07 15:36:40 -07:00
2017-04-20 22:58:35 +02:00
GncDateTime :: GncDateTime ( const GncDate & date , DayPart part ) :
m_impl ( new GncDateTimeImpl ( * ( date . m_impl ), part )) {}
2015-04-07 15:36:40 -07:00
void
GncDateTime :: now ()
{
m_impl -> now ();
}
2015-04-10 09:33:51 -07:00
GncDateTime :: operator time64 () const
{
return m_impl -> operator time64 ();
2015-04-07 15:36:40 -07:00
}
2015-04-26 16:44:39 -07:00
GncDateTime :: operator struct tm () const
{
return m_impl -> operator struct tm ();
}
long
GncDateTime :: offset () const
{
return m_impl -> offset ();
}
2015-04-26 18:01:23 -07:00
2015-04-28 15:23:26 -07:00
struct tm
GncDateTime :: utc_tm () const
{
return m_impl -> utc_tm ();
}
2015-04-28 10:06:18 -07:00
GncDate
GncDateTime :: date () const
{
2016-04-07 15:41:15 -07:00
return GncDate ( m_impl -> date ());
2015-04-28 10:06:18 -07:00
}
2015-04-26 18:01:23 -07:00
std :: string
GncDateTime :: format ( const char * format ) const
{
return m_impl -> format ( format );
}
2016-11-06 09:39:21 -08:00
std :: string
GncDateTime :: format_zulu ( const char * format ) const
{
return m_impl -> format_zulu ( format );
}
2017-04-20 21:42:24 +02:00
2018-12-28 16:39:02 -08:00
std :: string
GncDateTime :: format_iso8601 () const
{
return m_impl -> format_iso8601 ();
}
2019-01-05 14:53:25 -08:00
std :: string
GncDateTime :: timestamp ()
{
return GncDateTimeImpl :: timestamp ();
}
2017-04-20 21:42:24 +02:00
/* GncDate */
GncDate :: GncDate () : m_impl { new GncDateImpl } {}
GncDate :: GncDate ( int year , int month , int day ) :
m_impl ( new GncDateImpl ( year , month , day )) {}
2017-04-21 16:58:20 +02:00
GncDate :: GncDate ( const std :: string str , const std :: string fmt ) :
m_impl ( new GncDateImpl ( str , fmt )) {}
2017-04-20 21:42:24 +02:00
GncDate :: GncDate ( std :: unique_ptr < GncDateImpl > impl ) :
m_impl ( std :: move ( impl )) {}
2017-05-02 23:09:23 +02:00
GncDate :: GncDate ( const GncDate & a ) :
m_impl ( new GncDateImpl ( * a . m_impl )) {}
2017-04-20 21:42:24 +02:00
GncDate :: GncDate ( GncDate && ) = default ;
GncDate ::~ GncDate () = default ;
2017-05-02 23:09:23 +02:00
GncDate &
GncDate :: operator = ( const GncDate & a )
{
m_impl . reset ( new GncDateImpl ( * a . m_impl ));
return * this ;
}
2017-04-20 21:42:24 +02:00
GncDate &
GncDate :: operator = ( GncDate && ) = default ;
void
GncDate :: today ()
{
m_impl -> today ();
}
std :: string
GncDate :: format ( const char * format )
{
return m_impl -> format ( format );
}
2023-01-09 14:36:06 -08:00
gnc_ymd
2017-04-20 21:42:24 +02:00
GncDate :: year_month_day () const
{
return m_impl -> year_month_day ();
}
2017-04-29 18:01:12 +02:00
bool operator < ( const GncDate & a , const GncDate & b ) { return * ( a . m_impl ) < * ( b . m_impl ); }
bool operator > ( const GncDate & a , const GncDate & b ) { return * ( a . m_impl ) > * ( b . m_impl ); }
bool operator == ( const GncDate & a , const GncDate & b ) { return * ( a . m_impl ) == * ( b . m_impl ); }
bool operator <= ( const GncDate & a , const GncDate & b ) { return * ( a . m_impl ) <= * ( b . m_impl ); }
bool operator >= ( const GncDate & a , const GncDate & b ) { return * ( a . m_impl ) >= * ( b . m_impl ); }
bool operator != ( const GncDate & a , const GncDate & b ) { return * ( a . m_impl ) != * ( b . m_impl ); }