Files
2025-12-10 14:38:26 -08:00

422 lines
14 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#ifndef TIME_H_8457092814324342453627
#define TIME_H_8457092814324342453627
#include <ctime>
#include "basic_math.h"
#include "zstring.h"
namespace zen
{
struct TimeComp //replaces std::tm and SYSTEMTIME
{
int year = 0; // -
int month = 0; //1-12
int day = 0; //1-31
int hour = 0; //0-23
int minute = 0; //0-59
int second = 0; //0-60 (including leap second)
bool operator==(const TimeComp&) const = default;
};
TimeComp getUtcTime(time_t utc); //convert time_t (UTC) to UTC time components, returns TimeComp() on error
TimeComp getUtcTime(); //utc = std::time()
std::pair<time_t, bool /*success*/> utcToTimeT(const TimeComp& tc); //convert UTC time components to time_t (UTC)
TimeComp getLocalTime(time_t utc); //convert time_t (UTC) to local time components, returns TimeComp() on error
TimeComp getLocalTime(); //utc = std::time()
std::pair<time_t, bool /*success*/> localToTimeT(const TimeComp& tc); //convert local time components to time_t (UTC)
TimeComp getCompileTime(); //returns TimeComp() on error
//----------------------------------------------------------------------------------------------------------------------------------
/* format (current) date and time; example:
formatTime(Zstr("%Y|%m|%d")); -> "2011|10|29"
formatTime(formatDateTag); -> "2011-10-29"
formatTime(formatTimeTag); -> "17:55:34" */
Zstring formatTime(const Zchar* format, const TimeComp& tc = getLocalTime()); //format as specified by "std::strftime", returns empty string on error
//the "format" parameter of formatTime() is partially specialized with the following type tags:
const Zchar* const formatDateTag = Zstr("%x"); //locale-dependent date representation: e.g. 8/23/2001
const Zchar* const formatTimeTag = Zstr("%X"); //locale-dependent time representation: e.g. 2:55:02 PM
const Zchar* const formatDateTimeTag = Zstr("%c"); //locale-dependent date and time: e.g. 8/23/2001 2:55:02 PM
const Zchar* const formatIsoDateTag = Zstr("%Y-%m-%d"); //e.g. 2001-08-23
const Zchar* const formatIsoTimeTag = Zstr("%H:%M:%S"); //e.g. 14:55:02
const Zchar* const formatIsoDateTimeTag = Zstr("%Y-%m-%d %H:%M:%S"); //e.g. 2001-08-23 14:55:02
//----------------------------------------------------------------------------------------------------------------------------------
//example: parseTime("%Y-%m-%d %H:%M:%S", "2001-08-23 14:55:02");
// parseTime(formatIsoDateTimeTag, "2001-08-23 14:55:02");
template <class String, class String2>
TimeComp parseTime(const String& format, const String2& str); //similar to ::strptime()
//----------------------------------------------------------------------------------------------------------------------------------
//format: [-][[d.]HH:]MM:SS e.g. -1.23:45:67
Zstring formatTimeSpan(int64_t timeInSec, bool hourRequired = true);
//############################ implementation ##############################
namespace impl
{
inline
std::tm toClibTimeComponents(const TimeComp& tc)
{
assert(1 <= tc.month && tc.month <= 12 &&
1 <= tc.day && tc.day <= 31 &&
0 <= tc.hour && tc.hour <= 23 &&
0 <= tc.minute && tc.minute <= 59 &&
0 <= tc.second && tc.second <= 61);
return
{
.tm_sec = tc.second, //0-60 (including leap second)
.tm_min = tc.minute, //0-59
.tm_hour = tc.hour, //0-23
.tm_mday = tc.day, //1-31
.tm_mon = tc.month - 1, //0-11
.tm_year = tc.year - 1900, //years since 1900
.tm_isdst = -1, //> 0 if DST is active, == 0 if DST is not active, < 0 if the information is not available
//.tm_wday
//.tm_yday
};
}
inline
TimeComp toZenTimeComponents(const std::tm& ctc)
{
return
{
.year = ctc.tm_year + 1900,
.month = ctc.tm_mon + 1,
.day = ctc.tm_mday,
.hour = ctc.tm_hour,
.minute = ctc.tm_min,
.second = ctc.tm_sec,
};
}
/*
inline
bool isValid(const std::tm& t)
{
-> not enough! MSCRT has different limits than the C standard which even seem to change with different versions:
_VALIDATE_RETURN((( timeptr->tm_sec >=0 ) && ( timeptr->tm_sec <= 59 ) ), EINVAL, FALSE)
_VALIDATE_RETURN(( timeptr->tm_year >= -1900 ) && ( timeptr->tm_year <= 8099 ), EINVAL, FALSE)
-> also std::mktime does *not* help here at all!
auto inRange = [](int value, int minVal, int maxVal) { return minVal <= value && value <= maxVal; };
//https://www.cplusplus.com/reference/clibrary/ctime/tm/
return inRange(t.tm_sec, 0, 61) &&
inRange(t.tm_min, 0, 59) &&
inRange(t.tm_hour, 0, 23) &&
inRange(t.tm_mday, 1, 31) &&
inRange(t.tm_mon, 0, 11) &&
//tm_year
inRange(t.tm_wday, 0, 6) &&
inRange(t.tm_yday, 0, 365);
//tm_isdst
};
*/
}
constexpr auto daysPer400Years = 100 * (4 * 365 /*usual days per year*/ + 1 /*including leap day*/) - 3 /*no leap days for centuries, except if divisible by 400 */;
constexpr auto secsPer400Years = 3600LL * 24 * daysPer400Years;
inline
TimeComp getUtcTime(time_t utc)
{
//Windows: gmtime_s() only works for years [1970, 3001]
//=> map into working 400-year range [1970, 2370)
// bonus: avoid asking for bugs for time_t(-1)
const int cycles400 = static_cast<int>(numeric::intDivFloor(utc, secsPer400Years));
utc -= secsPer400Years * cycles400;
std::tm ctc = {};
if (::gmtime_r(&utc, &ctc) == nullptr) //Linux, macOS: apparently NO limits (tested years 0 to 10.000!)
return TimeComp();
ctc.tm_year += 400 * cycles400;
return impl::toZenTimeComponents(ctc);
}
inline
TimeComp getUtcTime()
{
const time_t utc = std::time(nullptr); //returns -1 on error
if (utc == -1)
return TimeComp();
return getUtcTime(utc);
}
inline
TimeComp getLocalTime(time_t utc)
{
const int cycles400 = static_cast<int>(numeric::intDivFloor(utc, secsPer400Years));
utc -= secsPer400Years * cycles400;
std::tm ctc = {};
if (::localtime_r(&utc, &ctc) == nullptr)
return TimeComp();
ctc.tm_year += 400 * cycles400;
return impl::toZenTimeComponents(ctc);
}
inline
TimeComp getLocalTime()
{
const time_t utc = std::time(nullptr); //returns -1 on error
if (utc == -1)
return TimeComp();
return getLocalTime(utc);
}
inline
std::pair<time_t, bool /*success*/> utcToTimeT(const TimeComp& tc)
{
if (tc == TimeComp())
return {};
std::tm ctc = impl::toClibTimeComponents(tc);
ctc.tm_isdst = 0; //"Zero (0) to indicate that standard time is in effect" => unused by _mkgmtime, but take no chances
/* Windows: _mkgmtime() only works for years [1970, 3001]
macOS: timegm() requires tm_year >= 1900; apparently no upper limit (tested until year 10.000!)
Linux, 64-bit: apparently NO limits (tested years 0 to 10.000!)
32-bit: timegm() only works for years [1902, 2038] => sucks to be on 32-bit! :>
=> map into working 400-year range [1970, 2370)
bonus: disambiguate -1 error code from time_t(-1) */
const int cycles400 = numeric::intDivFloor(ctc.tm_year + 1900 - 1970, 400);
ctc.tm_year -= 400 * cycles400;
const time_t utc = ::timegm(&ctc);
if (utc == -1)
return {};
assert(utc >= 0);
return {utc + secsPer400Years * cycles400, true};
}
inline
std::pair<time_t, bool /*success*/> localToTimeT(const TimeComp& tc) //convert local time components to time_t (UTC)
{
if (tc == TimeComp())
return {};
std::tm ctc = impl::toClibTimeComponents(tc);
const int cycles400 = numeric::intDivFloor(ctc.tm_year + 1900 - 1971/*[!]*/, 400); //see utcToTimeT()
//1971: ensures resulting time_t >= 0 after time zone, DST adaption, or std::mktime will fail on Windows!
ctc.tm_year -= 400 * cycles400;
const time_t locTime = std::mktime(&ctc);
if (locTime == -1)
return {};
assert(locTime > 0);
return {locTime + secsPer400Years * cycles400, true};
}
inline
TimeComp getCompileTime()
{
//https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html
char compileTime[] = __DATE__ " " __TIME__; //e.g. "Aug 1 2017 01:32:26"
if (compileTime[4] == ' ') //day is space-padded, but %d expects zero-padding
compileTime[4] = '0';
return parseTime("%b %d %Y %H:%M:%S", compileTime);
}
inline
Zstring formatTime(const Zchar* format, const TimeComp& tc)
{
if (tc == TimeComp()) //failure code from getLocalTime()
return Zstring();
std::tm ctc = impl::toClibTimeComponents(tc);
std::mktime(&ctc); //unfortunately std::strftime() needs all elements of "struct tm" filled, e.g. tm_wday, tm_yday
//note: although std::mktime() explicitly expects "local time", calculating weekday and day of year *should* be time-zone and DST independent
Zstring buf(256, Zstr('\0'));
//strftime() craziness on invalid input:
// VS 2010: CRASH unless "_invalid_parameter_handler" is set: https://docs.microsoft.com/en-us/cpp/c-runtime-library/parameter-validation
// GCC: returns 0, apparently no crash. Still, considering some clib maintainer's comments, we should expect the worst!
// Windows: avoid char-based strftime() which uses ANSI encoding! (e.g. Greek letters for AM/PM)
const size_t charsWritten = std::strftime(buf.data(), buf.size(), format, &ctc);
buf.resize(charsWritten);
return buf;
}
template <class String, class String2>
TimeComp parseTime(const String& format, const String2& str)
{
using CharType = GetCharTypeT<String>;
static_assert(std::is_same_v<CharType, GetCharTypeT<String2>>);
const CharType* itStr = strBegin(str);
const CharType* const strLast = itStr + strLength(str);
auto extractNumber = [&](int& result, size_t digitCount)
{
if (strLast - itStr < makeSigned(digitCount))
return false;
if (!std::all_of(itStr, itStr + digitCount, isDigit<CharType>))
return false;
result = zen::stringTo<int>(makeStringView(itStr, digitCount));
itStr += digitCount;
return true;
};
TimeComp output;
const CharType* itFmt = strBegin(format);
const CharType* const fmtLast = itFmt + strLength(format);
for (; itFmt != fmtLast; ++itFmt)
{
const CharType fmt = *itFmt;
if (fmt == '%')
{
++itFmt;
if (itFmt == fmtLast)
return TimeComp();
switch (*itFmt)
{
case 'Y':
if (!extractNumber(output.year, 4))
return TimeComp();
break;
case 'm':
if (!extractNumber(output.month, 2))
return TimeComp();
break;
case 'b': //abbreviated month name: Jan-Dec
{
if (strLast - itStr < 3)
return TimeComp();
const char* months[] = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"};
auto itMonth = std::find_if(std::begin(months), std::end(months), [&](const char* month)
{
return equalAsciiNoCase(makeStringView(itStr, 3), month);
});
if (itMonth == std::end(months))
return TimeComp();
output.month = 1 + static_cast<int>(itMonth - std::begin(months));
itStr += 3;
}
break;
case 'd':
if (!extractNumber(output.day, 2))
return TimeComp();
break;
case 'H':
if (!extractNumber(output.hour, 2))
return TimeComp();
break;
case 'M':
if (!extractNumber(output.minute, 2))
return TimeComp();
break;
case 'S':
if (!extractNumber(output.second, 2))
return TimeComp();
break;
default:
return TimeComp();
}
}
else if (isWhiteSpace(fmt)) //single whitespace in format => skip 0..n whitespace chars
{
while (itStr != strLast && isWhiteSpace(*itStr))
++itStr;
}
else
{
if (itStr == strLast || *itStr != fmt)
return TimeComp();
++itStr;
}
}
if (itStr != strLast)
return TimeComp();
return output;
}
inline
Zstring formatTimeSpan(int64_t timeInSec, bool hourRequired)
{
Zstring timespanStr;
if (timeInSec < 0)
{
timeInSec = -timeInSec; //need to fix LLONG_MIN?
timespanStr = Zstr('-');
}
//check *before* subtracting days!
const Zchar* timeSpanFmt = timeInSec < 3600 && !hourRequired ? Zstr("%M:%S") : formatIsoTimeTag;
const int secsPerDay = 24 * 3600;
const int64_t days = numeric::intDivFloor(timeInSec, secsPerDay);
if (days > 0)
{
timeInSec -= days * secsPerDay;
timespanStr += numberTo<Zstring>(days) + Zstr("."); //don't need zen::formatNumber(), do we?
}
//format time span as if absolute UTC time
const TimeComp& tc = getUtcTime(timeInSec); //returns TimeComp() on error
timespanStr += formatTime(timeSpanFmt, tc); //returns empty string on error
return timespanStr;
}
}
#endif //TIME_H_8457092814324342453627