View Issue Details

IDProjectCategoryView StatusLast Update
0001572OpenMPTGeneralpublic2022-07-09 13:42
Reportermanx Assigned Tomanx  
PrioritynormalSeverityminorReproducibilityhave not tried
Status resolvedResolutionfixed 
Product VersionOpenMPT 1.31.00.* (old testing) 
Target VersionOpenMPT 1.31.01.00 / libopenmpt 0.7.0 (upgrade first)Fixed in VersionOpenMPT 1.31.01.00 / libopenmpt 0.7.0 (upgrade first) 
Summary0001572: use more std::chrono and less ctime
Description

Getting rid of C time.h simplifies date calculations and solves all year2038 problems, amongst other things.

TagsNo tags attached.
Has the bug occurred in previous versions?
Tested code revision (in case you know it)

Relationships

related to 0001555 resolvedmanx bump minimum requirements to Windows 10 1903 (Build 18362) for Windows 10 builds 
child of 0001614 newmanx store Edit History in UTC 

Activities

manx

manx

2022-02-23 14:39

administrator   ~0005113

[Ref] mptTime: Use std::chrono for gregorian date calculations in C++20 mode.
[Ref] mptTime: Use std::chrono::system_clock for unix time instead of time_t in C++20 mode. This automatically makes all date handling year2038-correct in C++20 mode.
[Ref] mptTime: Use a custom gregorian date type mpt::Date::UTC instead of C struct tm.
[Ref] UpdateCheck: Use mpt::Date::Unix instead of time_t.
[Mod] UpdateCheck: Store last update timestamp as Unix seconds instead of YYYY-MM-DD HH:MM. On update, reset to 0.
[Mod] Edit History: For IT/MPTM, only ever store timestamps in UTC internally instead of localtime. Only convert to localtime when displaying to the user.
[Mod] Edit History: For other formats, also assume that the date is stored in UTC.
[Ref] mptTime: Disable std::chrono date handling completely for MSVC when targeting Windows earlier than Windows 10 1903 due to <https://github.com/microsoft/STL/issues/1911> and <https://github.com/microsoft/STL/issues/2163>.
[Ref] mptTime: Disable std::chrono for date parsing in VS2022 due to <https://developercommunity.visualstudio.com/t/stdchronoget-tzdb-list-memory-leak/1644641> / <https://github.com/microsoft/STL/issues/2504>.
chrono-v8.patch (34,303 bytes)   
Index: common/mptTime.cpp
===================================================================
--- common/mptTime.cpp	(revision 16961)
+++ common/mptTime.cpp	(working copy)
@@ -13,7 +13,12 @@
 
 #include "mptStringBuffer.h"
 
-#include <time.h>
+#if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+#include <chrono>
+#if 0
+#include <format>
+#endif
+#endif
 
 #if MPT_OS_WINDOWS
 #include <windows.h>
@@ -81,23 +86,8 @@
 
 #endif // MODPLUG_TRACKER
 
-Unix::Unix()
-	: Value(0)
-{
-	return;
-}
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
-Unix::Unix(int64 unixtime)
-	: Value(unixtime)
-{
-	return;
-}
-
-Unix::operator int64 () const
-{
-	return Value;
-}
-
 static int32 ToDaynum(int32 year, int32 month, int32 day)
 {
 	month = (month + 9) % 12;
@@ -128,77 +118,55 @@
 	day = static_cast<int32>(dd);
 }
 
-mpt::Date::Unix Unix::FromUTC(tm timeUtc)
+mpt::Date::Unix UnixFromUTC(UTC timeUtc)
 {
-	int32 daynum = ToDaynum(timeUtc.tm_year+1900, timeUtc.tm_mon+1, timeUtc.tm_mday);
-	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970,1,1))*24*60*60 + timeUtc.tm_hour*60*60 + timeUtc.tm_min*60 + timeUtc.tm_sec;
-	return mpt::Date::Unix(seconds);
+	int32 daynum = ToDaynum(timeUtc.year, timeUtc.month, timeUtc.day);
+	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970, 1, 1)) * 24 * 60 * 60 + timeUtc.hours * 60 * 60 + timeUtc.minutes * 60 + timeUtc.seconds;
+	return Unix{seconds};
 }
 
-tm Unix::AsUTC() const 
+mpt::Date::UTC UnixAsUTC(Unix tp)
 {
-	int64 tmp = Value;
+	int64 tmp = tp.value;
 	int64 seconds = tmp % 60; tmp /= 60;
 	int64 minutes = tmp % 60; tmp /= 60;
 	int64 hours   = tmp % 24; tmp /= 24;
 	int32 year = 0, month = 0, day = 0;
 	FromDaynum(static_cast<int32>(tmp) + ToDaynum(1970,1,1), year, month, day);
-	tm result = {};
-	result.tm_year = year - 1900;
-	result.tm_mon = month - 1;
-	result.tm_mday = day;
-	result.tm_hour = static_cast<int32>(hours);
-	result.tm_min = static_cast<int32>(minutes);
-	result.tm_sec = static_cast<int32>(seconds);
+	mpt::Date::UTC result = {};
+	result.year = year;
+	result.month = month;
+	result.day = day;
+	result.hours = static_cast<int32>(hours);
+	result.minutes = static_cast<int32>(minutes);
+	result.seconds = static_cast<int64>(seconds);
 	return result;
 }
 
-mpt::ustring ToShortenedISO8601(tm date)
+#endif
+
+mpt::ustring ToShortenedISO8601(mpt::Date::UTC date)
 {
-	// We assume date in UTC here.
-	// There are too many differences in supported format specifiers in strftime()
-	// and strftime does not support reduced precision ISO8601 at all.
-	// Just do the formatting ourselves.
 	mpt::ustring result;
 	mpt::ustring tz = U_("Z");
-	if(date.tm_year == 0)
+	if(date.year == 0)
 	{
 		return result;
 	}
-	result += mpt::ufmt::dec0<4>(date.tm_year + 1900);
-	if(date.tm_mon < 0 || date.tm_mon > 11)
+	result += mpt::ufmt::dec0<4>(date.year);
+	result += U_("-") + mpt::ufmt::dec0<2>(date.month);
+	result += U_("-") + mpt::ufmt::dec0<2>(date.day);
+	if(date.hours == 0 && date.minutes == 0 && date.seconds)
 	{
 		return result;
 	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mon + 1);
-	if(date.tm_mday < 1 || date.tm_mday > 31)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mday);
-	if(date.tm_hour == 0 && date.tm_min == 0 && date.tm_sec == 0)
-	{
-		return result;
-	}
-	if(date.tm_hour < 0 || date.tm_hour > 23)
-	{
-		return result;
-	}
-	if(date.tm_min < 0 || date.tm_min > 59)
-	{
-		return result;
-	}
 	result += U_("T");
-	if(date.tm_isdst > 0)
+	result += mpt::ufmt::dec0<2>(date.hours) + U_(":") + mpt::ufmt::dec0<2>(date.minutes);
+	if(date.seconds == 0)
 	{
-		tz = U_("+01:00");
-	}
-	result += mpt::ufmt::dec0<2>(date.tm_hour) + U_(":") + mpt::ufmt::dec0<2>(date.tm_min);
-	if(date.tm_sec < 0 || date.tm_sec > 61)
-	{
 		return result + tz;
 	}
-	result += U_(":") + mpt::ufmt::dec0<2>(date.tm_sec);
+	result += U_(":") + mpt::ufmt::dec0<2>(date.seconds);
 	result += tz;
 	return result;
 }
Index: common/mptTime.h
===================================================================
--- common/mptTime.h	(revision 16961)
+++ common/mptTime.h	(working copy)
@@ -12,9 +12,14 @@
 
 #include "openmpt/all/BuildSettings.hpp"
 
+#if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+#include <chrono>
+#endif
 #include <string>
 
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 #include <time.h>
+#endif
 
 
 OPENMPT_NAMESPACE_BEGIN
@@ -44,22 +49,124 @@
 
 #endif // MODPLUG_TRACKER
 
-class Unix
+struct UTC
 {
+	int          year    = 0;
+	unsigned int month   = 0;
+	unsigned int day     = 0;
+	int32        hours   = 0;
+	int32        minutes = 0;
+	int64        seconds = 0;
+	friend bool operator==(const UTC& lhs, const UTC& rhs)
+	{
+		return true
+			&& lhs.year == rhs.year
+			&& lhs.month == rhs.month
+			&& lhs.day == rhs.day
+			&& lhs.hours == rhs.hours
+			&& lhs.minutes == rhs.minutes
+			&& lhs.seconds == rhs.seconds
+			;
+	}
+	friend bool operator!=(const UTC& lhs, const UTC& rhs)
+	{
+		return false
+			|| lhs.year != rhs.year
+			|| lhs.month != rhs.month
+			|| lhs.day != rhs.day
+			|| lhs.hours != rhs.hours
+			|| lhs.minutes != rhs.minutes
+			|| lhs.seconds != rhs.seconds
+			;
+	}
+};
+
+#if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+
+using Unix = std::chrono::system_clock::time_point;
+
+inline Unix UnixNow()
+{
+	return std::chrono::system_clock::now();
+}
+
+inline int64 UnixAsSeconds(Unix tp)
+{
+	return std::chrono::duration_cast<std::chrono::seconds>(tp.time_since_epoch()).count();
+}
+
+inline Unix UnixFromSeconds(int64 seconds)
+{
+	return std::chrono::system_clock::time_point{std::chrono::seconds{seconds}};
+}
+
+inline mpt::Date::Unix UnixFromUTC(UTC utc)
+{
+	std::chrono::year_month_day ymd =
+		std::chrono::year{utc.year} /
+		std::chrono::month{utc.month} /
+		std::chrono::day{utc.day};
+	std::chrono::hh_mm_ss<std::chrono::seconds> hms{
+		std::chrono::hours{utc.hours} +
+		std::chrono::minutes{utc.minutes} +
+		std::chrono::seconds{utc.seconds}};
+	return std::chrono::system_clock::time_point{static_cast<std::chrono::sys_days>(ymd)} + hms.to_duration();
+}
+
+inline mpt::Date::UTC UnixAsUTC(Unix tp)
+{
+	std::chrono::sys_days dp = std::chrono::floor<std::chrono::days>(tp);
+	std::chrono::year_month_day ymd{dp};
+	std::chrono::hh_mm_ss hms{tp - dp};
+	mpt::Date::UTC result;
+	result.year = static_cast<int>(ymd.year());
+	result.month = static_cast<unsigned int>(ymd.month());
+	result.day = static_cast<unsigned int>(ymd.day());
+	result.hours = hms.hours().count();
+	result.minutes = hms.minutes().count();
+	result.seconds = hms.seconds().count();
+	return result;
+}
+
+#else
+
 // int64 counts 1s since 1970-01-01T00:00Z
-private:
-	int64 Value;
-public:
-	Unix();
-	explicit Unix(int64 unixtime);
-	operator int64 () const;
-public:
-	static mpt::Date::Unix FromUTC(tm timeUtc);
-	tm AsUTC() const;
+struct Unix
+{
+	int64 value{};
+	friend bool operator==(const Unix &a, const Unix &b)
+	{
+		return a.value == b.value;
+	}
+	friend bool operator!=(const Unix &a, const Unix &b)
+	{
+		return a.value != b.value;
+	}
 };
 
-mpt::ustring ToShortenedISO8601(tm date); // i.e. 2015-01-15T18:32:01Z
+inline Unix UnixNow()
+{
+	return Unix{static_cast<int64>(time(nullptr))};
+}
 
+inline int64 UnixAsSeconds(Unix tp)
+{
+	return tp.value;
+}
+
+inline Unix UnixFromSeconds(int64 seconds)
+{
+	return Unix{seconds};
+}
+
+mpt::Date::Unix UnixFromUTC(UTC timeUtc);
+
+mpt::Date::UTC UnixAsUTC(Unix tp);
+
+#endif
+
+mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
+
 } // namespace Date
 } // namespace mpt
 
Index: common/versionNumber.h
===================================================================
--- common/versionNumber.h	(revision 16961)
+++ common/versionNumber.h	(working copy)
@@ -18,6 +18,6 @@
 #define VER_MAJORMAJOR  1
 #define VER_MAJOR      31
 #define VER_MINOR      00
-#define VER_MINORMINOR 09
+#define VER_MINORMINOR 10
 
 OPENMPT_NAMESPACE_END
Index: mptrack/dlg_misc.cpp
===================================================================
--- mptrack/dlg_misc.cpp	(revision 16961)
+++ mptrack/dlg_misc.cpp	(working copy)
@@ -1311,18 +1311,8 @@
 	for(const auto &entry : editHistory)
 	{
 		totalTime += entry.openTime;
-
 		// Date
-		CString sDate;
-		if(entry.HasValidDate())
-		{
-			TCHAR szDate[32];
-			_tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &entry.loadDate);
-			sDate = szDate;
-		} else
-		{
-			sDate = _T("<unknown date>");
-		}
+		CString sDate = entry.HasValidDate() ? CTime(mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(entry.loadDate))).Format(_T("%d %b %Y, %H:%M:%S")) : CString(_T("<unknown date>"));
 		// Time + stuff
 		uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION);
 		s += MPT_CFORMAT("Loaded {}, open for {}h {}m {}s\r\n")(
Index: mptrack/MainFrm.cpp
===================================================================
--- mptrack/MainFrm.cpp	(revision 16961)
+++ mptrack/MainFrm.cpp	(working copy)
@@ -2784,8 +2784,10 @@
 	const bool isAutoUpdate = CUpdateCheck::IsAutoUpdateFromMessage(wparam, lparam);
 	const UpdateCheckResult &result = CUpdateCheck::MessageAsResult(wparam, lparam);
 	CUpdateCheck::AcknowledgeSuccess(result);
-	if(result.CheckTime != time_t{})
-		TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(result.CheckTime);
+	if(result.CheckTime != mpt::Date::Unix{})
+	{
+		TrackerSettings::Instance().UpdateLastUpdateCheck = result.CheckTime;
+	}
 	if(!isAutoUpdate)
 	{
 		if(m_UpdateOptionsDialog)
Index: mptrack/Moddoc.cpp
===================================================================
--- mptrack/Moddoc.cpp	(revision 16961)
+++ mptrack/Moddoc.cpp	(working copy)
@@ -133,7 +133,7 @@
 	, m_InstrumentUndo(*this)
 {
 	// Set the creation date of this file (or the load time if we're loading an existing file)
-	time(&m_creationTime);
+	m_creationTime = mpt::Date::UnixNow();
 
 	ReinitRecordState();
 
Index: mptrack/Moddoc.h
===================================================================
--- mptrack/Moddoc.h	(revision 16961)
+++ mptrack/Moddoc.h	(working copy)
@@ -14,10 +14,10 @@
 
 #include "Sndfile.h"
 #include "../common/misc_util.h"
+#include "../common/mptTime.h"
 #include "Undo.h"
 #include "Notification.h"
 #include "UpdateHints.h"
-#include <time.h>
 
 OPENMPT_NAMESPACE_BEGIN
 
@@ -130,7 +130,7 @@
 	CSampleUndo m_SampleUndo;
 	CInstrumentUndo m_InstrumentUndo;
 	SplitKeyboardSettings m_SplitKeyboardSettings;	// this is maybe not the best place to keep them, but it should do the job
-	time_t m_creationTime;
+	mpt::Date::Unix m_creationTime;
 
 	std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave?
 
@@ -221,7 +221,7 @@
 	CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; }
 	SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; }
 
-	time_t GetCreationTime() const { return m_creationTime; }
+	mpt::Date::Unix GetCreationTime() const { return m_creationTime; }
 
 // operations
 public:
Index: mptrack/TrackerSettings.cpp
===================================================================
--- mptrack/TrackerSettings.cpp	(revision 16961)
+++ mptrack/TrackerSettings.cpp	(working copy)
@@ -326,7 +326,7 @@
 	// Update
 	, UpdateEnabled(conf, U_("Update"), U_("Enabled"), true)
 	, UpdateInstallAutomatically(conf, U_("Update"), U_("InstallAutomatically"), false)
-	, UpdateLastUpdateCheck(conf, U_("Update"), U_("LastUpdateCheck"), mpt::Date::Unix(time_t()))
+	, UpdateLastUpdateCheck(conf, U_("Update"), U_("LastUpdateCheck"), mpt::Date::Unix{})
 	, UpdateUpdateCheckPeriod_DEPRECATED(conf, U_("Update"), U_("UpdateCheckPeriod"), 7)
 	, UpdateIntervalDays(conf, U_("Update"), U_("UpdateCheckIntervalDays"), 7)
 	, UpdateChannel(conf, U_("Update"), U_("Channel"), UpdateChannelRelease)
@@ -822,6 +822,10 @@
 		conf.Forget(UpdateUpdateURL_DEPRECATED.GetPath());
 		conf.Forget(UpdateSendGUID_DEPRECATED.GetPath());
 	}
+	if(storedVersion < MPT_V("1.31.00.10"))
+	{
+		UpdateLastUpdateCheck = mpt::Date::Unix{};
+	}
 #endif // MPT_ENABLE_UPDATE
 
 	if(storedVersion < MPT_V("1.29.00.39"))
Index: mptrack/TrackerSettings.h
===================================================================
--- mptrack/TrackerSettings.h	(revision 16961)
+++ mptrack/TrackerSettings.h	(working copy)
@@ -471,32 +471,12 @@
 
 template<> inline SettingValue ToSettingValue(const mpt::Date::Unix &val)
 {
-	time_t t = val;
-	const tm* lastUpdate = gmtime(&t);
-	CString outDate;
-	if(lastUpdate)
-	{
-		outDate.Format(_T("%04d-%02d-%02d %02d:%02d"), lastUpdate->tm_year + 1900, lastUpdate->tm_mon + 1, lastUpdate->tm_mday, lastUpdate->tm_hour, lastUpdate->tm_min);
-	}
-	return SettingValue(mpt::ToUnicode(outDate), "UTC");
+	return SettingValue(mpt::ufmt::val(mpt::Date::UnixAsSeconds(val)), "UnixTime");
 }
 template<> inline mpt::Date::Unix FromSettingValue(const SettingValue &val)
 {
-	MPT_ASSERT(val.GetTypeTag() == "UTC");
-	std::string s = mpt::ToCharset(mpt::Charset::Locale, val.as<mpt::ustring>());
-	tm lastUpdate;
-	MemsetZero(lastUpdate);
-	if(sscanf(s.c_str(), "%04d-%02d-%02d %02d:%02d", &lastUpdate.tm_year, &lastUpdate.tm_mon, &lastUpdate.tm_mday, &lastUpdate.tm_hour, &lastUpdate.tm_min) == 5)
-	{
-		lastUpdate.tm_year -= 1900;
-		lastUpdate.tm_mon--;
-	}
-	time_t outTime = mpt::Date::Unix::FromUTC(lastUpdate);
-	if(outTime < 0)
-	{
-		outTime = 0;
-	}
-	return mpt::Date::Unix(outTime);
+	MPT_ASSERT(val.GetTypeTag() == "UnixTime");
+	return mpt::Date::UnixFromSeconds(mpt::ConvertStringTo<int64>(val.as<mpt::ustring>()));
 }
 
 struct FontSetting
Index: mptrack/UpdateCheck.cpp
===================================================================
--- mptrack/UpdateCheck.cpp	(revision 16961)
+++ mptrack/UpdateCheck.cpp	(working copy)
@@ -453,11 +453,11 @@
 			return;
 		}
 		// Do we actually need to run the update check right now?
-		const time_t now = time(nullptr);
-		const time_t lastCheck = TrackerSettings::Instance().UpdateLastUpdateCheck.Get();
+		const mpt::Date::Unix now = mpt::Date::UnixNow();
+		const mpt::Date::Unix lastCheck = TrackerSettings::Instance().UpdateLastUpdateCheck.Get();
 		// Check update interval. Note that we always check for updates when the system time had gone backwards (i.e. when the last update check supposedly happened in the future).
-		const double secsSinceLastCheck = difftime(now, lastCheck);
-		if(secsSinceLastCheck > 0.0 && secsSinceLastCheck < updateCheckPeriod * 86400.0)
+		const int64 secsSinceLastCheck = mpt::Date::UnixAsSeconds(now) - mpt::Date::UnixAsSeconds(lastCheck);
+		if(secsSinceLastCheck > 0 && secsSinceLastCheck < updateCheckPeriod * 86400)
 		{
 			loadPersisted = true;
 		}
@@ -475,7 +475,7 @@
 				);
 			if(Reporting::Confirm(msg, _T("OpenMPT Update")) == cnfNo)
 			{
-				TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(now);
+				TrackerSettings::Instance().UpdateLastUpdateCheck = now;
 				return;
 			}
 		}
@@ -650,7 +650,7 @@
 				{
 					std::vector<std::byte> data = GetFileReader(f).ReadRawDataAsByteVector();
 					nlohmann::json::parse(mpt::buffer_cast<std::string>(data)).get<Update::versions>();
-					result.CheckTime = time_t{};
+					result.CheckTime = mpt::Date::Unix{};
 					result.json = data;
 					loaded = true;
 				}
@@ -781,7 +781,7 @@
 
 	// Now, evaluate the downloaded data.
 	UpdateCheckResult result;
-	result.CheckTime = time(nullptr);
+	result.CheckTime = mpt::Date::UnixNow();
 	try
 	{
 		nlohmann::json::parse(mpt::buffer_cast<std::string>(resultHTTP.Data)).get<Update::versions>();
@@ -1315,7 +1315,7 @@
 {
 	if(!result.IsFromCache())
 	{
-		TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(result.CheckTime);
+		TrackerSettings::Instance().UpdateLastUpdateCheck = result.CheckTime;
 	}
 }
 
@@ -1612,14 +1612,10 @@
 	if(changedPath == TrackerSettings::Instance().UpdateLastUpdateCheck.GetPath())
 	{
 		CString updateText;
-		const time_t t = TrackerSettings::Instance().UpdateLastUpdateCheck.Get();
-		if(t > 0)
+		const mpt::Date::Unix t = TrackerSettings::Instance().UpdateLastUpdateCheck.Get();
+		if(t != mpt::Date::Unix{})
 		{
-			const tm* const lastUpdate = localtime(&t);
-			if(lastUpdate != nullptr)
-			{
-				updateText.Format(_T("The last successful update check was run on %04d-%02d-%02d, %02d:%02d."), lastUpdate->tm_year + 1900, lastUpdate->tm_mon + 1, lastUpdate->tm_mday, lastUpdate->tm_hour, lastUpdate->tm_min);
-			}
+			updateText += CTime(mpt::Date::UnixAsSeconds(t)).Format(_T("The last successful update check was run on %F, %R."));
 		}
 		updateText += _T("\r\n");
 		SetDlgItemText(IDC_LASTUPDATE, updateText);
Index: mptrack/UpdateCheck.h
===================================================================
--- mptrack/UpdateCheck.h	(revision 16961)
+++ mptrack/UpdateCheck.h	(working copy)
@@ -14,7 +14,7 @@
 
 #include "mpt/uuid/uuid.hpp"
 
-#include <time.h>
+#include "../common/mptTime.h"
 
 #include <atomic>
 
@@ -40,9 +40,12 @@
 
 struct UpdateCheckResult
 {
-	time_t CheckTime = time_t{};
+	mpt::Date::Unix CheckTime = mpt::Date::Unix{};
 	std::vector<std::byte> json;
-	bool IsFromCache() const noexcept { return CheckTime == time_t{}; }
+	bool IsFromCache() const noexcept
+	{
+		return CheckTime == mpt::Date::Unix{};
+	}
 };
 
 class CUpdateCheck
Index: soundlib/ITTools.cpp
===================================================================
--- soundlib/ITTools.cpp	(revision 16961)
+++ soundlib/ITTools.cpp	(working copy)
@@ -637,15 +637,15 @@
 void ITHistoryStruct::ConvertToMPT(FileHistory &mptHistory) const
 {
 	// Decode FAT date and time
-	MemsetZero(mptHistory.loadDate);
+	mptHistory.loadDate = mpt::Date::UTC{};
 	if(fatdate != 0 || fattime != 0)
 	{
-		mptHistory.loadDate.tm_year = ((fatdate >> 9) & 0x7F) + 80;
-		mptHistory.loadDate.tm_mon = Clamp((fatdate >> 5) & 0x0F, 1, 12) - 1;
-		mptHistory.loadDate.tm_mday = Clamp(fatdate & 0x1F, 1, 31);
-		mptHistory.loadDate.tm_hour = Clamp((fattime >> 11) & 0x1F, 0, 23);
-		mptHistory.loadDate.tm_min = Clamp((fattime >> 5) & 0x3F, 0, 59);
-		mptHistory.loadDate.tm_sec = Clamp((fattime & 0x1F) * 2, 0, 59);
+		mptHistory.loadDate.year = ((fatdate >> 9) & 0x7F) + 1980;
+		mptHistory.loadDate.month = Clamp((fatdate >> 5) & 0x0F, 1, 12);
+		mptHistory.loadDate.day = Clamp(fatdate & 0x1F, 1, 31);
+		mptHistory.loadDate.hours = Clamp((fattime >> 11) & 0x1F, 0, 23);
+		mptHistory.loadDate.minutes = Clamp((fattime >> 5) & 0x3F, 0, 59);
+		mptHistory.loadDate.seconds = Clamp((fattime & 0x1F) * 2, 0, 59);
 	}
 	mptHistory.openTime = static_cast<uint32>(runtime * (HISTORY_TIMER_PRECISION / 18.2));
 }
@@ -657,8 +657,8 @@
 	// Create FAT file dates
 	if(mptHistory.HasValidDate())
 	{
-		fatdate = static_cast<uint16>(mptHistory.loadDate.tm_mday | ((mptHistory.loadDate.tm_mon + 1) << 5) | ((mptHistory.loadDate.tm_year - 80) << 9));
-		fattime = static_cast<uint16>((mptHistory.loadDate.tm_sec / 2) | (mptHistory.loadDate.tm_min << 5) | (mptHistory.loadDate.tm_hour << 11));
+		fatdate = static_cast<uint16>(mptHistory.loadDate.day | (mptHistory.loadDate.month << 5) | ((mptHistory.loadDate.year - 1980) << 9));
+		fattime = static_cast<uint16>((mptHistory.loadDate.seconds / 2) | (mptHistory.loadDate.minutes << 5) | (mptHistory.loadDate.hours << 11));
 	} else
 	{
 		fatdate = 0;
Index: soundlib/Load_dmf.cpp
===================================================================
--- soundlib/Load_dmf.cpp	(revision 16961)
+++ soundlib/Load_dmf.cpp	(working copy)
@@ -911,9 +911,9 @@
 	m_songArtist = mpt::ToUnicode(mpt::Charset::CP437, mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.composer));
 
 	FileHistory mptHistory;
-	mptHistory.loadDate.tm_mday = Clamp(fileHeader.creationDay, uint8(1), uint8(31));
-	mptHistory.loadDate.tm_mon = Clamp(fileHeader.creationMonth, uint8(1), uint8(12)) - 1;
-	mptHistory.loadDate.tm_year = fileHeader.creationYear;
+	mptHistory.loadDate.day = Clamp(fileHeader.creationDay, uint8(1), uint8(31));
+	mptHistory.loadDate.month = Clamp(fileHeader.creationMonth, uint8(1), uint8(12));
+	mptHistory.loadDate.year = 1900 + fileHeader.creationYear;
 	m_FileHistory.clear();
 	m_FileHistory.push_back(mptHistory);
 
Index: soundlib/Load_gt2.cpp
===================================================================
--- soundlib/Load_gt2.cpp	(revision 16961)
+++ soundlib/Load_gt2.cpp	(working copy)
@@ -1214,9 +1214,9 @@
 	m_playBehaviour.set(kApplyOffsetWithoutNote);
 
 	FileHistory mptHistory;
-	mptHistory.loadDate.tm_mday = Clamp<uint8, uint8>(fileHeader.day, 1, 31);
-	mptHistory.loadDate.tm_mon = Clamp<uint8, uint8>(fileHeader.month, 1, 12) - 1;
-	mptHistory.loadDate.tm_year = fileHeader.year - 1900;
+	mptHistory.loadDate.day = Clamp<uint8, uint8>(fileHeader.day, 1, 31);
+	mptHistory.loadDate.month = Clamp<uint8, uint8>(fileHeader.month, 1, 12);
+	mptHistory.loadDate.year = fileHeader.year;
 	m_FileHistory.push_back(mptHistory);
 
 	m_nChannels = 32;
Index: soundlib/Load_it.cpp
===================================================================
--- soundlib/Load_it.cpp	(revision 16961)
+++ soundlib/Load_it.cpp	(working copy)
@@ -1352,17 +1352,9 @@
 		} else if(pModDoc != nullptr)
 		{
 			// Current ("new") timestamp
-			const time_t creationTime = pModDoc->GetCreationTime();
-
-			MemsetZero(mptHistory.loadDate);
-			//localtime_s(&loadDate, &creationTime);
-			const tm* const p = localtime(&creationTime);
-			if (p != nullptr)
-				mptHistory.loadDate = *p;
-			else
-				sndFile.AddToLog(LogError, U_("Unable to retrieve current time."));
-
-			mptHistory.openTime = (uint32)(difftime(time(nullptr), creationTime) * HISTORY_TIMER_PRECISION);
+			const mpt::Date::Unix creationTime = pModDoc->GetCreationTime();
+			mptHistory.loadDate = mpt::Date::UnixAsUTC(creationTime);
+			mptHistory.openTime = static_cast<uint32>(mpt::round((mpt::Date::UnixAsSeconds(mpt::Date::UnixNow()) - mpt::Date::UnixAsSeconds(creationTime)) * HISTORY_TIMER_PRECISION));
 #endif // MODPLUG_TRACKER
 		}
 
Index: soundlib/Load_mod.cpp
===================================================================
--- soundlib/Load_mod.cpp	(revision 16961)
+++ soundlib/Load_mod.cpp	(working copy)
@@ -2180,12 +2180,12 @@
 		   && mpt::is_in_range(info.dateMinute, 0, 59) && mpt::is_in_range(info.dateSecond, 0, 59))
 		{
 			FileHistory mptHistory;
-			mptHistory.loadDate.tm_year = info.dateYear;
-			mptHistory.loadDate.tm_mon = info.dateMonth - 1;
-			mptHistory.loadDate.tm_mday = info.dateDay;
-			mptHistory.loadDate.tm_hour = info.dateHour;
-			mptHistory.loadDate.tm_min = info.dateMinute;
-			mptHistory.loadDate.tm_sec = info.dateSecond;
+			mptHistory.loadDate.year = info.dateYear + 1900;
+			mptHistory.loadDate.month = info.dateMonth;
+			mptHistory.loadDate.day = info.dateDay;
+			mptHistory.loadDate.hours = info.dateHour;
+			mptHistory.loadDate.minutes = info.dateMinute;
+			mptHistory.loadDate.seconds = info.dateSecond;
 			m_FileHistory.push_back(mptHistory);
 		}
 	}
Index: soundlib/Sndfile.cpp
===================================================================
--- soundlib/Sndfile.cpp	(revision 16961)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -56,15 +56,15 @@
 
 mpt::ustring FileHistory::AsISO8601() const
 {
-	tm date = loadDate;
+	mpt::Date::UTC date = loadDate;
 	if(openTime > 0)
 	{
 		// Calculate the date when editing finished.
 		double openSeconds = static_cast<double>(openTime) / HISTORY_TIMER_PRECISION;
-		tm tmpLoadDate = loadDate;
-		int64 loadDateSinceEpoch = mpt::Date::Unix::FromUTC(tmpLoadDate);
+		mpt::Date::UTC tmpLoadDate = loadDate;
+		int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(tmpLoadDate));
 		int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
-		date = mpt::Date::Unix(saveDateSinceEpoch).AsUTC();
+		date = mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch));
 	}
 	return mpt::Date::ToShortenedISO8601(date);
 }
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 16961)
+++ soundlib/Sndfile.h	(working copy)
@@ -243,13 +243,16 @@
 struct FileHistory
 {
 	// Date when the file was loaded in the the tracker or created.
-	tm loadDate = {};
+	mpt::Date::UTC loadDate = {};
 	// Time the file was open in the editor, in 1/18.2th seconds (frequency of a standard DOS timer, to keep compatibility with Impulse Tracker easy).
 	uint32 openTime = 0;
 	// Return the date as a (possibly truncated if not enough precision is available) ISO 8601 formatted date.
 	mpt::ustring AsISO8601() const;
 	// Returns true if the date component is valid. Some formats only store edit time, not edit date.
-	bool HasValidDate() const { return loadDate.tm_mday != 0; }
+	bool HasValidDate() const
+	{
+		return loadDate != mpt::Date::UTC{};
+	}
 };
 
 
Index: src/mpt/base/detect_quirks.hpp
===================================================================
--- src/mpt/base/detect_quirks.hpp	(revision 16961)
+++ src/mpt/base/detect_quirks.hpp	(working copy)
@@ -145,4 +145,34 @@
 
 
 
+#if MPT_CXX_AT_LEAST(20)
+#if MPT_LIBCXX_MS && MPT_OS_WINDOWS
+#if defined(NTDDI_VERSION)
+#if (NTDDI_VERSION < 0x0A000007) // < Windows 10 1903
+// std::chrono timezones require Windows 10 1903 with VS2022 as of 2022-01-22.
+// See <https://github.com/microsoft/STL/issues/1911>
+// and <https://github.com/microsoft/STL/issues/2163>.
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
+#endif
+#else
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
+#endif
+#endif
+#if MPT_LIBCXX_GNU_BEFORE(11)
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
+#elif MPT_LIBCXX_LLVM_BEFORE(7000)
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
+#endif
+#if MPT_LIBCXX_MS
+// Causes massive memory leaks.
+// See <https://developercommunity.visualstudio.com/t/stdchronoget-tzdb-list-memory-leak/1644641>
+// / <https://github.com/microsoft/STL/issues/2504>.
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#elif MPT_LIBCXX_GNU
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#endif
+#endif
+
+
+
 #endif // MPT_BASE_DETECT_QUIRKS_HPP
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 16961)
+++ test/test.cpp	(working copy)
@@ -777,48 +777,15 @@
 }
 
 
-namespace {
-
-struct Gregorian {
-	int Y,M,D,h,m,s;
-	static Gregorian FromTM(tm t) {
-		Gregorian g;
-		g.Y = t.tm_year + 1900;
-		g.M = t.tm_mon + 1;
-		g.D = t.tm_mday;
-		g.h = t.tm_hour;
-		g.m = t.tm_min;
-		g.s = t.tm_sec;
-		return g;
-	}
-	static tm ToTM(Gregorian g) {
-		tm t;
-		MemsetZero(t);
-		t.tm_year = g.Y - 1900;
-		t.tm_mon = g.M - 1;
-		t.tm_mday = g.D;
-		t.tm_hour = g.h;
-		t.tm_min = g.m;
-		t.tm_sec = g.s;
-		return t;
-	}
-};
-
-inline bool operator ==(Gregorian a, Gregorian b) {
-	return a.Y == b.Y && a.M == b.M && a.D == b.D && a.h == b.h && a.m == b.m && a.s == b.s;
+static int64 TestDate1(int s, int m, int h, unsigned int D, unsigned int M, int Y) {
+	return mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::UTC{Y,M,D,h,m,s}));
 }
 
+static mpt::Date::UTC TestDate2(int s, int m, int h, unsigned int D, unsigned int M, int Y) {
+	return mpt::Date::UTC{Y,M,D,h,m,s};
 }
 
-static int64 TestDate1(int s, int m, int h, int D, int M, int Y) {
-	return mpt::Date::Unix::FromUTC(Gregorian::ToTM(Gregorian{Y,M,D,h,m,s}));
-}
 
-static Gregorian TestDate2(int s, int m, int h, int D, int M, int Y) {
-	return Gregorian{Y,M,D,h,m,s};
-}
-
-
 static MPT_NOINLINE void TestMisc1()
 {
 
@@ -1086,30 +1053,30 @@
 	VERIFY_EQUAL(    1413064016, TestDate1( 56, 46, 21, 11, 10, 2014 ));
 	VERIFY_EQUAL(    1413064100, TestDate1( 20, 48, 21, 11, 10, 2014 ));
 
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(             0).AsUTC()), TestDate2(  0,  0,  0,  1,  1, 1970 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(          3600).AsUTC()), TestDate2(  0,  0,  1,  1,  1, 1970 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(         86400).AsUTC()), TestDate2(  0,  0,  0,  2,  1, 1970 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(      31536000).AsUTC()), TestDate2(  0,  0,  0,  1,  1, 1971 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(     100000000).AsUTC()), TestDate2( 40, 46,  9,  3,  3, 1973 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(     951782400).AsUTC()), TestDate2(  0,  0,  0, 29,  2, 2000 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1000000000).AsUTC()), TestDate2( 40, 46,  1,  9,  9, 2001 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1044057600).AsUTC()), TestDate2(  0,  0,  0,  1,  2, 2003 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1044144000).AsUTC()), TestDate2(  0,  0,  0,  2,  2, 2003 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1046476800).AsUTC()), TestDate2(  0,  0,  0,  1,  3, 2003 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1064966400).AsUTC()), TestDate2(  0,  0,  0,  1, 10, 2003 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1077926399).AsUTC()), TestDate2( 59, 59, 23, 27,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1077926400).AsUTC()), TestDate2(  0,  0,  0, 28,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1077926410).AsUTC()), TestDate2( 10,  0,  0, 28,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078012799).AsUTC()), TestDate2( 59, 59, 23, 28,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078012800).AsUTC()), TestDate2(  0,  0,  0, 29,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078012820).AsUTC()), TestDate2( 20,  0,  0, 29,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078099199).AsUTC()), TestDate2( 59, 59, 23, 29,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078099200).AsUTC()), TestDate2(  0,  0,  0,  1,  3, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078099230).AsUTC()), TestDate2( 30,  0,  0,  1,  3, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078185599).AsUTC()), TestDate2( 59, 59, 23,  1,  3, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1096588800).AsUTC()), TestDate2(  0,  0,  0,  1, 10, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1413064016).AsUTC()), TestDate2( 56, 46, 21, 11, 10, 2014 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1413064100).AsUTC()), TestDate2( 20, 48, 21, 11, 10, 2014 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(             0)), TestDate2(  0,  0,  0,  1,  1, 1970 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(          3600)), TestDate2(  0,  0,  1,  1,  1, 1970 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(         86400)), TestDate2(  0,  0,  0,  2,  1, 1970 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(      31536000)), TestDate2(  0,  0,  0,  1,  1, 1971 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(     100000000)), TestDate2( 40, 46,  9,  3,  3, 1973 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(     951782400)), TestDate2(  0,  0,  0, 29,  2, 2000 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1000000000)), TestDate2( 40, 46,  1,  9,  9, 2001 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1044057600)), TestDate2(  0,  0,  0,  1,  2, 2003 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1044144000)), TestDate2(  0,  0,  0,  2,  2, 2003 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1046476800)), TestDate2(  0,  0,  0,  1,  3, 2003 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1064966400)), TestDate2(  0,  0,  0,  1, 10, 2003 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1077926399)), TestDate2( 59, 59, 23, 27,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1077926400)), TestDate2(  0,  0,  0, 28,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1077926410)), TestDate2( 10,  0,  0, 28,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078012799)), TestDate2( 59, 59, 23, 28,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078012800)), TestDate2(  0,  0,  0, 29,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078012820)), TestDate2( 20,  0,  0, 29,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078099199)), TestDate2( 59, 59, 23, 29,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078099200)), TestDate2(  0,  0,  0,  1,  3, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078099230)), TestDate2( 30,  0,  0,  1,  3, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078185599)), TestDate2( 59, 59, 23,  1,  3, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1096588800)), TestDate2(  0,  0,  0,  1, 10, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1413064016)), TestDate2( 56, 46, 21, 11, 10, 2014 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1413064100)), TestDate2( 20, 48, 21, 11, 10, 2014 ));
 
 
 #ifdef MODPLUG_TRACKER
@@ -2069,12 +2036,12 @@
 	// Edit history
 	VERIFY_EQUAL_NONCONT(sndFile.GetFileHistory().size() > 15, true);
 	const FileHistory &fh = sndFile.GetFileHistory().front();
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_year, 111);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_mon, 5);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_mday, 14);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_hour, 21);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_min, 8);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_sec, 32);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
 	VERIFY_EQUAL_NONCONT((uint32)((double)fh.openTime / HISTORY_TIMER_PRECISION), 31);
 
 	// Macros
chrono-v8.patch (34,303 bytes)   
manx

manx

2022-05-28 05:20

administrator   ~0005193

Except for EditHistory, this has been partially merged as r17377, r17379, r17380, r17383.

manx

manx

2022-06-04 16:34

administrator   ~0005195

[Mod] Edit History: Dates in Edit History can now have 1 of 3 timezone designators: UTC, Local, Unspecified.
[Mod] Edit History: When the timezone is Unspecified, dates are now printed without the UTC timezone signifier 'Z' in ISO8601 format.
[Mod] IT/MPTM: Dates are now written in UTC. Loading decides by looking at m_dwLastSavedWithVersion how to interpret dates. After loading, all dates with Local timezone are converted into UTC timezone, dates with Unspecified timezone are kept as is.
[Mod] MOD: Assume dates are in Local timezone.
[Imp] When calculating save time from load time and open duration, properly take Local timezone into account.
[Ref] Edit History: OpenMPT still displays dates in Local timezone, and assumes so also for Unspecified timezone dates.

Todo:

  • Work-around tzdb memleak in VS2022 STL.
chrono-v12.patch (22,283 bytes)   
Index: common/mptTime.cpp
===================================================================
--- common/mptTime.cpp	(revision 17409)
+++ common/mptTime.cpp	(working copy)
@@ -83,7 +83,7 @@
 
 #endif // MODPLUG_TRACKER
 
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) || defined(MPT_TIME_CTIME)
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 static int32 ToDaynum(int32 year, int32 month, int32 day)
 {
@@ -115,10 +115,6 @@
 	day = static_cast<int32>(dd);
 }
 
-#endif
-
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
-
 mpt::Date::Unix UnixFromUTC(UTC timeUtc)
 {
 	int32 daynum = ToDaynum(timeUtc.year, timeUtc.month, timeUtc.day);
@@ -144,6 +140,40 @@
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+mpt::Date::Unix UnixFromLocal(Local timeLocal)
+{
+	std::tm tmp{};
+	tmp.tm_year = timeLocal.year - 1900;
+	tmp.tm_mon = timeLocal.month - 1;
+	tmp.tm_mday = timeLocal.day;
+	tmp.tm_hour = timeLocal.hours;
+	tmp.tm_min = timeLocal.minutes;
+	tmp.tm_sec = timeLocal.seconds;
+	return static_cast<int64>(std::mktime(&tmp));
+}
+
+mpt::Date::Local UnixAsLocal(Local tp)
+{
+	std::tm *tmp = std::localtime(static_cast<std::time_t>(tp));
+	if(!tmp)
+	{
+		return mpt::Date::Local{};
+	}
+	std::tm local = *tmp;
+	mpt::Date::Local result{};
+	result.year = local.tm_year + 1900;
+	result.mon = local.tm_month + 1;
+	result.day = local.tm_mday;
+	result.hours = local.tm_hour;
+	result.minutes = local.tm_min;
+	result.seconds = local.tm_sec;
+	return result;
+}
+
+#endif // MODPLUG_TRACKER
+
 #endif
 
 template <LogicalTimezone TZ>
@@ -193,85 +223,13 @@
 	return ToShortenedISO8601Impl(date);
 }
 
-#if defined(MPT_TIME_CTIME)
-
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc)
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date)
 {
-	int32 daynum = ToDaynum(timeUtc.tm_year+1900, timeUtc.tm_mon+1, timeUtc.tm_mday);
-	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970,1,1))*24*60*60 + timeUtc.tm_hour*60*60 + timeUtc.tm_min*60 + timeUtc.tm_sec;
-	return mpt::Date::UnixFromSeconds(seconds);
+	return ToShortenedISO8601Impl(date);
 }
+#endif // MODPLUG_TRACKER
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime)
-{
-	int64 tmp = mpt::Date::UnixAsSeconds(unixtime);
-	int64 seconds = tmp % 60; tmp /= 60;
-	int64 minutes = tmp % 60; tmp /= 60;
-	int64 hours   = tmp % 24; tmp /= 24;
-	int32 year = 0, month = 0, day = 0;
-	FromDaynum(static_cast<int32>(tmp) + ToDaynum(1970,1,1), year, month, day);
-	tm result = {};
-	result.tm_year = year - 1900;
-	result.tm_mon = month - 1;
-	result.tm_mday = day;
-	result.tm_hour = static_cast<int32>(hours);
-	result.tm_min = static_cast<int32>(minutes);
-	result.tm_sec = static_cast<int32>(seconds);
-	return result;
-}
-
-mpt::ustring ToShortenedISO8601(tm date)
-{
-	// We assume date in UTC here.
-	// There are too many differences in supported format specifiers in strftime()
-	// and strftime does not support reduced precision ISO8601 at all.
-	// Just do the formatting ourselves.
-	mpt::ustring result;
-	mpt::ustring tz = U_("Z");
-	if(date.tm_year == 0)
-	{
-		return result;
-	}
-	result += mpt::ufmt::dec0<4>(date.tm_year + 1900);
-	if(date.tm_mon < 0 || date.tm_mon > 11)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mon + 1);
-	if(date.tm_mday < 1 || date.tm_mday > 31)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mday);
-	if(date.tm_hour == 0 && date.tm_min == 0 && date.tm_sec == 0)
-	{
-		return result;
-	}
-	if(date.tm_hour < 0 || date.tm_hour > 23)
-	{
-		return result;
-	}
-	if(date.tm_min < 0 || date.tm_min > 59)
-	{
-		return result;
-	}
-	result += U_("T");
-	if(date.tm_isdst > 0)
-	{
-		tz = U_("+01:00");
-	}
-	result += mpt::ufmt::dec0<2>(date.tm_hour) + U_(":") + mpt::ufmt::dec0<2>(date.tm_min);
-	if(date.tm_sec < 0 || date.tm_sec > 61)
-	{
-		return result + tz;
-	}
-	result += U_(":") + mpt::ufmt::dec0<2>(date.tm_sec);
-	result += tz;
-	return result;
-}
-
-#endif
-
 } // namespace Date
 } // namespace mpt
 
Index: common/mptTime.h
===================================================================
--- common/mptTime.h	(revision 17409)
+++ common/mptTime.h	(working copy)
@@ -21,13 +21,7 @@
 #include <time.h>
 #endif
 
-#define MPT_TIME_CTIME
 
-#if defined(MPT_TIME_CTIME)
-#include <time.h>
-#endif
-
-
 OPENMPT_NAMESPACE_BEGIN
 
 
@@ -99,37 +93,38 @@
 
 using AnyGregorian = Gregorian<LogicalTimezone::Unspecified>;
 
-#if defined(MPT_TIME_CTIME)
-inline tm AsTm(AnyGregorian val)
+using UTC = Gregorian<LogicalTimezone::UTC>;
+
+#if defined(MODPLUG_TRACKER)
+using Local = Gregorian<LogicalTimezone::Local>;
+#endif // MODPLUG_TRACKER
+
+template <LogicalTimezone TZ>
+inline Gregorian<TZ> interpret_as_timezone(AnyGregorian gregorian)
 {
-	tm result{};
-	result.tm_year = val.year - 1900;
-	result.tm_mon = val.month - 1;
-	result.tm_mday = val.day;
-	result.tm_hour = val.hours;
-	result.tm_min = val.minutes;
-	result.tm_sec = static_cast<int>(val.seconds);
+	Gregorian<TZ> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-inline AnyGregorian AsGregorian(tm val)
+
+template <LogicalTimezone TZ>
+inline Gregorian<LogicalTimezone::Unspecified> forget_timezone(Gregorian<TZ> gregorian)
 {
-	AnyGregorian result{};
-	result.year = val.tm_year + 1900;
-	result.month = val.tm_mon + 1;
-	result.day = val.tm_mday;
-	result.hours = val.tm_hour;
-	result.minutes = val.tm_min;
-	result.seconds = val.tm_sec;
+	Gregorian<LogicalTimezone::Unspecified> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-#endif
 
-using UTC = Gregorian<LogicalTimezone::UTC>;
-
-#if defined(MODPLUG_TRACKER)
-using Local = Gregorian<LogicalTimezone::Local>;
-#endif // MODPLUG_TRACKER
-
 #if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 using Unix = std::chrono::system_clock::time_point;
@@ -151,15 +146,15 @@
 
 inline mpt::Date::Unix UnixFromUTC(UTC utc)
 {
-	std::chrono::year_month_day ymd =
-		std::chrono::year{utc.year} /
-		std::chrono::month{utc.month} /
-		std::chrono::day{utc.day};
-	std::chrono::hh_mm_ss<std::chrono::seconds> hms{
-		std::chrono::hours{utc.hours} +
-		std::chrono::minutes{utc.minutes} +
-		std::chrono::seconds{utc.seconds}};
-	return std::chrono::system_clock::time_point{static_cast<std::chrono::sys_days>(ymd)} + hms.to_duration();
+	return std::chrono::system_clock::time_point{
+		std::chrono::sys_days {
+			std::chrono::year{ utc.year } /
+			std::chrono::month{ utc.month } /
+			std::chrono::day{ utc.day }
+		} +
+		std::chrono::hours{ utc.hours } +
+		std::chrono::minutes{ utc.minutes } +
+		std::chrono::seconds{ utc.seconds }};
 }
 
 inline mpt::Date::UTC UnixAsUTC(Unix tp)
@@ -177,6 +172,40 @@
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+inline mpt::Date::Unix UnixFromLocal(Local local)
+{
+	std::chrono::time_point<std::chrono::local_t, std::chrono::seconds> local_tp = 
+		std::chrono::local_days {
+			std::chrono::year{ local.year } /
+			std::chrono::month{ local.month } /
+			std::chrono::day{ local.day }
+		} +
+		std::chrono::hours{ local.hours } +
+		std::chrono::minutes{ local.minutes } +
+		std::chrono::seconds{ local.seconds };
+	return std::chrono::zoned_time{std::chrono::current_zone(), local_tp}.get_sys_time();
+}
+
+inline mpt::Date::Local UnixAsLocal(Unix tp)
+{
+	std::chrono::zoned_time local_tp{ std::chrono::current_zone(), tp };
+	std::chrono::local_days dp = std::chrono::floor<std::chrono::days>(local_tp.get_local_time());
+	std::chrono::year_month_day ymd{dp};
+	std::chrono::hh_mm_ss hms{local_tp.get_local_time() - dp};
+	mpt::Date::Local result;
+	result.year = static_cast<int>(ymd.year());
+	result.month = static_cast<unsigned int>(ymd.month());
+	result.day = static_cast<unsigned int>(ymd.day());
+	result.hours = hms.hours().count();
+	result.minutes = hms.minutes().count();
+	result.seconds = hms.seconds().count();
+	return result;
+}
+
+#endif // MODPLUG_TRACKER
+
 #else
 
 // int64 counts 1s since 1970-01-01T00:00Z
@@ -195,7 +224,7 @@
 
 inline Unix UnixNow()
 {
-	return Unix{static_cast<int64>(time(nullptr))};
+	return Unix{static_cast<int64>(std::time(nullptr))};
 }
 
 inline int64 UnixAsSeconds(Unix tp)
@@ -212,21 +241,23 @@
 
 mpt::Date::UTC UnixAsUTC(Unix tp);
 
-#endif
+#if defined(MODPLUG_TRACKER)
 
-mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
+mpt::Date::Unix UnixFromLocal(Local timeLocal);
 
-mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
+mpt::Date::Local UnixAsLocal(Local tp);
 
-#if defined(MPT_TIME_CTIME)
+#endif // MODPLUG_TRACKER
 
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc);
+#endif
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime);
+mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
 
-mpt::ustring ToShortenedISO8601(tm date); // i.e. 2015-01-15T18:32:01Z
+mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
 
-#endif
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date); // i.e. 2015-01-15T18:32:01
+#endif // MODPLUG_TRACKER
 
 } // namespace Date
 } // namespace mpt
Index: common/versionNumber.h
===================================================================
--- common/versionNumber.h	(revision 17409)
+++ common/versionNumber.h	(working copy)
@@ -18,6 +18,6 @@
 #define VER_MAJORMAJOR  1
 #define VER_MAJOR      31
 #define VER_MINOR      00
-#define VER_MINORMINOR 12
+#define VER_MINORMINOR 13
 
 OPENMPT_NAMESPACE_END
Index: libopenmpt/libopenmpt_impl.cpp
===================================================================
--- libopenmpt/libopenmpt_impl.cpp	(revision 17409)
+++ libopenmpt/libopenmpt_impl.cpp	(working copy)
@@ -1241,7 +1241,7 @@
 		if ( m_sndFile->GetFileHistory().empty() || !m_sndFile->GetFileHistory().back().HasValidDate() ) {
 			return std::string();
 		}
-		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601() );
+		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601( m_sndFile->GetTimezoneInternal() ) );
 	} else if ( key == std::string("message") ) {
 		std::string retval = m_sndFile->m_songMessage.GetFormatted( OpenMPT::SongMessage::leLF );
 		if ( retval.empty() ) {
Index: mptrack/dlg_misc.cpp
===================================================================
--- mptrack/dlg_misc.cpp	(revision 17409)
+++ mptrack/dlg_misc.cpp	(working copy)
@@ -1311,18 +1311,15 @@
 	for(const auto &entry : editHistory)
 	{
 		totalTime += entry.openTime;
-
 		// Date
-		CString sDate;
+		CString sDate = CString(_T("<unknown date>"));
 		if(entry.HasValidDate())
 		{
-			TCHAR szDate[32];
-			const tm loadDate = mpt::Date::AsTm(entry.loadDate);
-			_tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &loadDate);
-			sDate = szDate;
-		} else
-		{
-			sDate = _T("<unknown date>");
+			const mpt::Date::Unix unixdate = ((m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local) || (m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Unspecified))
+				? mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(entry.loadDate))
+				: mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(entry.loadDate));
+				;
+			sDate = CTime(mpt::Date::UnixAsSeconds(unixdate)).Format(_T("%d %b %Y, %H:%M:%S"));
 		}
 		// Time + stuff
 		uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION);
Index: mptrack/Moddoc.cpp
===================================================================
--- mptrack/Moddoc.cpp	(revision 17409)
+++ mptrack/Moddoc.cpp	(working copy)
@@ -133,7 +133,7 @@
 	, m_InstrumentUndo(*this)
 {
 	// Set the creation date of this file (or the load time if we're loading an existing file)
-	time(&m_creationTime);
+	m_creationTime = mpt::Date::UnixNow();
 
 	ReinitRecordState();
 
Index: mptrack/Moddoc.h
===================================================================
--- mptrack/Moddoc.h	(revision 17409)
+++ mptrack/Moddoc.h	(working copy)
@@ -14,10 +14,10 @@
 
 #include "Sndfile.h"
 #include "../common/misc_util.h"
+#include "../common/mptTime.h"
 #include "Undo.h"
 #include "Notification.h"
 #include "UpdateHints.h"
-#include <time.h>
 
 OPENMPT_NAMESPACE_BEGIN
 
@@ -130,7 +130,7 @@
 	CSampleUndo m_SampleUndo;
 	CInstrumentUndo m_InstrumentUndo;
 	SplitKeyboardSettings m_SplitKeyboardSettings;	// this is maybe not the best place to keep them, but it should do the job
-	time_t m_creationTime;
+	mpt::Date::Unix m_creationTime;
 
 	std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave?
 
@@ -221,7 +221,7 @@
 	CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; }
 	SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; }
 
-	time_t GetCreationTime() const { return m_creationTime; }
+	mpt::Date::Unix GetCreationTime() const { return m_creationTime; }
 
 // operations
 public:
Index: mptrack/OPLExport.cpp
===================================================================
--- mptrack/OPLExport.cpp	(revision 17409)
+++ mptrack/OPLExport.cpp	(working copy)
@@ -397,7 +397,7 @@
 		SetDlgItemText(IDC_EDIT2, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.GetTitle()).c_str());
 		SetDlgItemText(IDC_EDIT3, mpt::ToWin(m_sndFile.m_songArtist).c_str());
 		if(!m_sndFile.GetFileHistory().empty())
-			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601().substr(0, 10), U_("-"), U_("/"))).c_str());
+			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601(m_sndFile.GetTimezoneInternal()).substr(0, 10), U_("-"), U_("/"))).c_str());
 		SetDlgItemText(IDC_EDIT5, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.m_songMessage.GetFormatted(SongMessage::leCRLF)).c_str());
 
 		m_locked = false;
Index: soundlib/Load_it.cpp
===================================================================
--- soundlib/Load_it.cpp	(revision 17409)
+++ soundlib/Load_it.cpp	(working copy)
@@ -1278,6 +1278,11 @@
 	m_modFormat.type = (GetType() == MOD_TYPE_MPT) ? U_("mptm") : U_("it");
 	m_modFormat.madeWithTracker = std::move(madeWithTracker);
 	m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::Charset::Windows1252 : mpt::Charset::CP437;
+#ifdef MODPLUG_TRACKER
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_V("1.31.00.13"))) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Local;
+#else
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_V("1.31.00.13"))) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Unspecified;
+#endif
 
 	return true;
 }
@@ -1359,15 +1364,19 @@
 		} else if(pModDoc != nullptr)
 		{
 			// Current ("new") timestamp
-			const time_t creationTime = pModDoc->GetCreationTime();
-			mptHistory.loadDate = mpt::Date::AnyGregorian{};
-			//localtime_s(&loadDate, &creationTime);
-			const tm* const p = localtime(&creationTime);
-			if (p != nullptr)
-				mptHistory.loadDate = mpt::Date::AsGregorian(*p);
-			else
-				sndFile.AddToLog(LogError, U_("Unable to retrieve current time."));
-			mptHistory.openTime = (uint32)(difftime(time(nullptr), creationTime) * HISTORY_TIMER_PRECISION);
+			const mpt::Date::Unix creationTime = pModDoc->GetCreationTime();
+			if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			} else if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(creationTime));
+			} else
+			{
+				// assume UTC
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			}
+			mptHistory.openTime = static_cast<uint32>(mpt::round((mpt::Date::UnixAsSeconds(mpt::Date::UnixNow()) - mpt::Date::UnixAsSeconds(creationTime)) * HISTORY_TIMER_PRECISION));
 #endif // MODPLUG_TRACKER
 		}
 
Index: soundlib/Load_mod.cpp
===================================================================
--- soundlib/Load_mod.cpp	(revision 17409)
+++ soundlib/Load_mod.cpp	(working copy)
@@ -2179,6 +2179,11 @@
 		if(mpt::is_in_range(info.dateMonth, 1, 12) && mpt::is_in_range(info.dateDay, 1, 31) && mpt::is_in_range(info.dateHour, 0, 23)
 		   && mpt::is_in_range(info.dateMinute, 0, 59) && mpt::is_in_range(info.dateSecond, 0, 59))
 		{
+#ifdef MODPLUG_TRACKER
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Local;
+#else
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Unspecified;
+#endif
 			FileHistory mptHistory;
 			mptHistory.loadDate.year = info.dateYear + 1900;
 			mptHistory.loadDate.month = info.dateMonth;
Index: soundlib/Sndfile.cpp
===================================================================
--- soundlib/Sndfile.cpp	(revision 17409)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -54,19 +54,47 @@
 }
 
 
-mpt::ustring FileHistory::AsISO8601() const
+mpt::ustring FileHistory::AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const
 {
-	tm date = mpt::Date::AsTm(loadDate);
 	if(openTime > 0)
 	{
 		// Calculate the date when editing finished.
 		double openSeconds = static_cast<double>(openTime) / HISTORY_TIMER_PRECISION;
-		tm tmpLoadDate = mpt::Date::AsTm(loadDate);
-		int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTCtm(tmpLoadDate));
-		int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
-		date = mpt::Date::UnixAsUTCtm(mpt::Date::UnixFromSeconds(saveDateSinceEpoch));
+		mpt::Date::AnyGregorian tmpLoadDate = loadDate;
+		if (internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsLocal(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			// assume UTC for unspecified timezone when calculating
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch))));
+		}
+	} else
+	{
+		if(internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(loadDate));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(loadDate));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			return mpt::Date::ToShortenedISO8601(loadDate);
+		}
 	}
-	return mpt::Date::ToShortenedISO8601(date);
 }
 
 
@@ -483,8 +511,24 @@
 		InitializeGlobals();
 		m_visitedRows.Initialize(true);
 		m_dwCreatedWithVersion = Version::Current();
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
 	}
 
+#ifdef MODPLUG_TRACKER
+	// convert timestamps to UTC
+	if(m_modFormat.timezone == mpt::Date::LogicalTimezone::Local)
+	{
+		for(auto & fileHistoryEntry : m_FileHistory)
+		{
+			if(fileHistoryEntry.HasValidDate())
+			{
+				fileHistoryEntry.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(fileHistoryEntry.loadDate))));
+			}
+		}
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
+	}
+#endif // MODPLUG_TRACKER
+
 	// Adjust channels
 	const auto muteFlag = GetChannelMuteFlag();
 	for(CHANNELINDEX chn = 0; chn < MAX_BASECHANNELS; chn++)
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 17409)
+++ soundlib/Sndfile.h	(working copy)
@@ -247,7 +247,7 @@
 	// Time the file was open in the editor, in 1/18.2th seconds (frequency of a standard DOS timer, to keep compatibility with Impulse Tracker easy).
 	uint32 openTime = 0;
 	// Return the date as a (possibly truncated if not enough precision is available) ISO 8601 formatted date.
-	mpt::ustring AsISO8601() const;
+	mpt::ustring AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const;
 	// Returns true if the date component is valid. Some formats only store edit time, not edit date.
 	bool HasValidDate() const
 	{
@@ -285,6 +285,7 @@
 	mpt::ustring originalFormatName; // "FastTracker 2" in the case of converted formats like MO3 or GDM
 	mpt::ustring originalType;       // "xm" in the case of converted formats like MO3 or GDM
 	mpt::Charset charset = mpt::Charset::UTF8;
+	mpt::Date::LogicalTimezone timezone = mpt::Date::LogicalTimezone::Unspecified;
 };
 
 
@@ -752,6 +753,10 @@
 			return GetCharsetFile();
 		#endif // MODPLUG_TRACKER
 	}
+	mpt::Date::LogicalTimezone GetTimezoneInternal() const
+	{
+		return m_modFormat.timezone;
+	}
 
 	ModMessageHeuristicOrder GetMessageHeuristic() const;
 
chrono-v12.patch (22,283 bytes)   
manx

manx

2022-06-04 18:25

administrator   ~0005196

[Mod] Edit History: Dates in Edit History can now have 1 of 3 timezone designators: UTC, Local, Unspecified.
[Mod] Edit History: When the timezone is Unspecified, dates are now printed without the UTC timezone signifier 'Z' in ISO8601 format.
[Mod] IT/MPTM: Dates are now written in UTC. Loading decides by looking at m_dwLastSavedWithVersion how to interpret dates. After loading, all dates with Local timezone are converted into UTC timezone, dates with Unspecified timezone are kept as is.
[Mod] MOD: Assume dates are in Local timezone.
[Imp] When calculating save time from load time and open duration, properly take Local timezone into account.
[Ref] Edit History: OpenMPT still displays dates in Local timezone, and assumes so also for Unspecified timezone dates.
[Fix] Work-around MSVC tzdb memleak. See &lt;https://github.com/microsoft/STL/issues/2504#issuecomment-1068008937>.
[Ref] mptTime: Remove all &lt;ctime> usage for modern builds.
chrono-v13.patch (26,451 bytes)   
Index: common/mptTime.cpp
===================================================================
--- common/mptTime.cpp	(revision 17411)
+++ common/mptTime.cpp	(working copy)
@@ -83,7 +83,7 @@
 
 #endif // MODPLUG_TRACKER
 
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) || defined(MPT_TIME_CTIME)
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 static int32 ToDaynum(int32 year, int32 month, int32 day)
 {
@@ -115,10 +115,6 @@
 	day = static_cast<int32>(dd);
 }
 
-#endif
-
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
-
 mpt::Date::Unix UnixFromUTC(UTC timeUtc)
 {
 	int32 daynum = ToDaynum(timeUtc.year, timeUtc.month, timeUtc.day);
@@ -144,6 +140,40 @@
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+mpt::Date::Unix UnixFromLocal(Local timeLocal)
+{
+	std::tm tmp{};
+	tmp.tm_year = timeLocal.year - 1900;
+	tmp.tm_mon = timeLocal.month - 1;
+	tmp.tm_mday = timeLocal.day;
+	tmp.tm_hour = timeLocal.hours;
+	tmp.tm_min = timeLocal.minutes;
+	tmp.tm_sec = timeLocal.seconds;
+	return static_cast<int64>(std::mktime(&tmp));
+}
+
+mpt::Date::Local UnixAsLocal(Local tp)
+{
+	std::tm *tmp = std::localtime(static_cast<std::time_t>(tp));
+	if(!tmp)
+	{
+		return mpt::Date::Local{};
+	}
+	std::tm local = *tmp;
+	mpt::Date::Local result{};
+	result.year = local.tm_year + 1900;
+	result.mon = local.tm_month + 1;
+	result.day = local.tm_mday;
+	result.hours = local.tm_hour;
+	result.minutes = local.tm_min;
+	result.seconds = local.tm_sec;
+	return result;
+}
+
+#endif // MODPLUG_TRACKER
+
 #endif
 
 template <LogicalTimezone TZ>
@@ -193,85 +223,13 @@
 	return ToShortenedISO8601Impl(date);
 }
 
-#if defined(MPT_TIME_CTIME)
-
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc)
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date)
 {
-	int32 daynum = ToDaynum(timeUtc.tm_year+1900, timeUtc.tm_mon+1, timeUtc.tm_mday);
-	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970,1,1))*24*60*60 + timeUtc.tm_hour*60*60 + timeUtc.tm_min*60 + timeUtc.tm_sec;
-	return mpt::Date::UnixFromSeconds(seconds);
+	return ToShortenedISO8601Impl(date);
 }
+#endif // MODPLUG_TRACKER
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime)
-{
-	int64 tmp = mpt::Date::UnixAsSeconds(unixtime);
-	int64 seconds = tmp % 60; tmp /= 60;
-	int64 minutes = tmp % 60; tmp /= 60;
-	int64 hours   = tmp % 24; tmp /= 24;
-	int32 year = 0, month = 0, day = 0;
-	FromDaynum(static_cast<int32>(tmp) + ToDaynum(1970,1,1), year, month, day);
-	tm result = {};
-	result.tm_year = year - 1900;
-	result.tm_mon = month - 1;
-	result.tm_mday = day;
-	result.tm_hour = static_cast<int32>(hours);
-	result.tm_min = static_cast<int32>(minutes);
-	result.tm_sec = static_cast<int32>(seconds);
-	return result;
-}
-
-mpt::ustring ToShortenedISO8601(tm date)
-{
-	// We assume date in UTC here.
-	// There are too many differences in supported format specifiers in strftime()
-	// and strftime does not support reduced precision ISO8601 at all.
-	// Just do the formatting ourselves.
-	mpt::ustring result;
-	mpt::ustring tz = U_("Z");
-	if(date.tm_year == 0)
-	{
-		return result;
-	}
-	result += mpt::ufmt::dec0<4>(date.tm_year + 1900);
-	if(date.tm_mon < 0 || date.tm_mon > 11)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mon + 1);
-	if(date.tm_mday < 1 || date.tm_mday > 31)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mday);
-	if(date.tm_hour == 0 && date.tm_min == 0 && date.tm_sec == 0)
-	{
-		return result;
-	}
-	if(date.tm_hour < 0 || date.tm_hour > 23)
-	{
-		return result;
-	}
-	if(date.tm_min < 0 || date.tm_min > 59)
-	{
-		return result;
-	}
-	result += U_("T");
-	if(date.tm_isdst > 0)
-	{
-		tz = U_("+01:00");
-	}
-	result += mpt::ufmt::dec0<2>(date.tm_hour) + U_(":") + mpt::ufmt::dec0<2>(date.tm_min);
-	if(date.tm_sec < 0 || date.tm_sec > 61)
-	{
-		return result + tz;
-	}
-	result += U_(":") + mpt::ufmt::dec0<2>(date.tm_sec);
-	result += tz;
-	return result;
-}
-
-#endif
-
 } // namespace Date
 } // namespace mpt
 
Index: common/mptTime.h
===================================================================
--- common/mptTime.h	(revision 17411)
+++ common/mptTime.h	(working copy)
@@ -21,13 +21,7 @@
 #include <time.h>
 #endif
 
-#define MPT_TIME_CTIME
 
-#if defined(MPT_TIME_CTIME)
-#include <time.h>
-#endif
-
-
 OPENMPT_NAMESPACE_BEGIN
 
 
@@ -99,37 +93,38 @@
 
 using AnyGregorian = Gregorian<LogicalTimezone::Unspecified>;
 
-#if defined(MPT_TIME_CTIME)
-inline tm AsTm(AnyGregorian val)
+using UTC = Gregorian<LogicalTimezone::UTC>;
+
+#if defined(MODPLUG_TRACKER)
+using Local = Gregorian<LogicalTimezone::Local>;
+#endif // MODPLUG_TRACKER
+
+template <LogicalTimezone TZ>
+inline Gregorian<TZ> interpret_as_timezone(AnyGregorian gregorian)
 {
-	tm result{};
-	result.tm_year = val.year - 1900;
-	result.tm_mon = val.month - 1;
-	result.tm_mday = val.day;
-	result.tm_hour = val.hours;
-	result.tm_min = val.minutes;
-	result.tm_sec = static_cast<int>(val.seconds);
+	Gregorian<TZ> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-inline AnyGregorian AsGregorian(tm val)
+
+template <LogicalTimezone TZ>
+inline Gregorian<LogicalTimezone::Unspecified> forget_timezone(Gregorian<TZ> gregorian)
 {
-	AnyGregorian result{};
-	result.year = val.tm_year + 1900;
-	result.month = val.tm_mon + 1;
-	result.day = val.tm_mday;
-	result.hours = val.tm_hour;
-	result.minutes = val.tm_min;
-	result.seconds = val.tm_sec;
+	Gregorian<LogicalTimezone::Unspecified> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-#endif
 
-using UTC = Gregorian<LogicalTimezone::UTC>;
-
-#if defined(MODPLUG_TRACKER)
-using Local = Gregorian<LogicalTimezone::Local>;
-#endif // MODPLUG_TRACKER
-
 #if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 using Unix = std::chrono::system_clock::time_point;
@@ -151,15 +146,15 @@
 
 inline mpt::Date::Unix UnixFromUTC(UTC utc)
 {
-	std::chrono::year_month_day ymd =
-		std::chrono::year{utc.year} /
-		std::chrono::month{utc.month} /
-		std::chrono::day{utc.day};
-	std::chrono::hh_mm_ss<std::chrono::seconds> hms{
-		std::chrono::hours{utc.hours} +
-		std::chrono::minutes{utc.minutes} +
-		std::chrono::seconds{utc.seconds}};
-	return std::chrono::system_clock::time_point{static_cast<std::chrono::sys_days>(ymd)} + hms.to_duration();
+	return std::chrono::system_clock::time_point{
+		std::chrono::sys_days {
+			std::chrono::year{ utc.year } /
+			std::chrono::month{ utc.month } /
+			std::chrono::day{ utc.day }
+		} +
+		std::chrono::hours{ utc.hours } +
+		std::chrono::minutes{ utc.minutes } +
+		std::chrono::seconds{ utc.seconds }};
 }
 
 inline mpt::Date::UTC UnixAsUTC(Unix tp)
@@ -177,6 +172,40 @@
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+inline mpt::Date::Unix UnixFromLocal(Local local)
+{
+	std::chrono::time_point<std::chrono::local_t, std::chrono::seconds> local_tp = 
+		std::chrono::local_days {
+			std::chrono::year{ local.year } /
+			std::chrono::month{ local.month } /
+			std::chrono::day{ local.day }
+		} +
+		std::chrono::hours{ local.hours } +
+		std::chrono::minutes{ local.minutes } +
+		std::chrono::seconds{ local.seconds };
+	return std::chrono::zoned_time{std::chrono::current_zone(), local_tp}.get_sys_time();
+}
+
+inline mpt::Date::Local UnixAsLocal(Unix tp)
+{
+	std::chrono::zoned_time local_tp{ std::chrono::current_zone(), tp };
+	std::chrono::local_days dp = std::chrono::floor<std::chrono::days>(local_tp.get_local_time());
+	std::chrono::year_month_day ymd{dp};
+	std::chrono::hh_mm_ss hms{local_tp.get_local_time() - dp};
+	mpt::Date::Local result;
+	result.year = static_cast<int>(ymd.year());
+	result.month = static_cast<unsigned int>(ymd.month());
+	result.day = static_cast<unsigned int>(ymd.day());
+	result.hours = hms.hours().count();
+	result.minutes = hms.minutes().count();
+	result.seconds = hms.seconds().count();
+	return result;
+}
+
+#endif // MODPLUG_TRACKER
+
 #else
 
 // int64 counts 1s since 1970-01-01T00:00Z
@@ -195,7 +224,7 @@
 
 inline Unix UnixNow()
 {
-	return Unix{static_cast<int64>(time(nullptr))};
+	return Unix{static_cast<int64>(std::time(nullptr))};
 }
 
 inline int64 UnixAsSeconds(Unix tp)
@@ -212,21 +241,23 @@
 
 mpt::Date::UTC UnixAsUTC(Unix tp);
 
-#endif
+#if defined(MODPLUG_TRACKER)
 
-mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
+mpt::Date::Unix UnixFromLocal(Local timeLocal);
 
-mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
+mpt::Date::Local UnixAsLocal(Local tp);
 
-#if defined(MPT_TIME_CTIME)
+#endif // MODPLUG_TRACKER
 
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc);
+#endif
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime);
+mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
 
-mpt::ustring ToShortenedISO8601(tm date); // i.e. 2015-01-15T18:32:01Z
+mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
 
-#endif
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date); // i.e. 2015-01-15T18:32:01
+#endif // MODPLUG_TRACKER
 
 } // namespace Date
 } // namespace mpt
Index: common/versionNumber.h
===================================================================
--- common/versionNumber.h	(revision 17411)
+++ common/versionNumber.h	(working copy)
@@ -18,6 +18,6 @@
 #define VER_MAJORMAJOR  1
 #define VER_MAJOR      31
 #define VER_MINOR      00
-#define VER_MINORMINOR 12
+#define VER_MINORMINOR 13
 
 OPENMPT_NAMESPACE_END
Index: libopenmpt/libopenmpt_impl.cpp
===================================================================
--- libopenmpt/libopenmpt_impl.cpp	(revision 17411)
+++ libopenmpt/libopenmpt_impl.cpp	(working copy)
@@ -1241,7 +1241,7 @@
 		if ( m_sndFile->GetFileHistory().empty() || !m_sndFile->GetFileHistory().back().HasValidDate() ) {
 			return std::string();
 		}
-		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601() );
+		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601( m_sndFile->GetTimezoneInternal() ) );
 	} else if ( key == std::string("message") ) {
 		std::string retval = m_sndFile->m_songMessage.GetFormatted( OpenMPT::SongMessage::leLF );
 		if ( retval.empty() ) {
Index: mptrack/dlg_misc.cpp
===================================================================
--- mptrack/dlg_misc.cpp	(revision 17411)
+++ mptrack/dlg_misc.cpp	(working copy)
@@ -1311,18 +1311,15 @@
 	for(const auto &entry : editHistory)
 	{
 		totalTime += entry.openTime;
-
 		// Date
-		CString sDate;
+		CString sDate = CString(_T("<unknown date>"));
 		if(entry.HasValidDate())
 		{
-			TCHAR szDate[32];
-			const tm loadDate = mpt::Date::AsTm(entry.loadDate);
-			_tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &loadDate);
-			sDate = szDate;
-		} else
-		{
-			sDate = _T("<unknown date>");
+			const mpt::Date::Unix unixdate = ((m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local) || (m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Unspecified))
+				? mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(entry.loadDate))
+				: mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(entry.loadDate));
+				;
+			sDate = CTime(mpt::Date::UnixAsSeconds(unixdate)).Format(_T("%d %b %Y, %H:%M:%S"));
 		}
 		// Time + stuff
 		uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION);
Index: mptrack/Moddoc.cpp
===================================================================
--- mptrack/Moddoc.cpp	(revision 17411)
+++ mptrack/Moddoc.cpp	(working copy)
@@ -133,7 +133,7 @@
 	, m_InstrumentUndo(*this)
 {
 	// Set the creation date of this file (or the load time if we're loading an existing file)
-	time(&m_creationTime);
+	m_creationTime = mpt::Date::UnixNow();
 
 	ReinitRecordState();
 
Index: mptrack/Moddoc.h
===================================================================
--- mptrack/Moddoc.h	(revision 17411)
+++ mptrack/Moddoc.h	(working copy)
@@ -14,10 +14,10 @@
 
 #include "Sndfile.h"
 #include "../common/misc_util.h"
+#include "../common/mptTime.h"
 #include "Undo.h"
 #include "Notification.h"
 #include "UpdateHints.h"
-#include <time.h>
 
 OPENMPT_NAMESPACE_BEGIN
 
@@ -130,7 +130,7 @@
 	CSampleUndo m_SampleUndo;
 	CInstrumentUndo m_InstrumentUndo;
 	SplitKeyboardSettings m_SplitKeyboardSettings;	// this is maybe not the best place to keep them, but it should do the job
-	time_t m_creationTime;
+	mpt::Date::Unix m_creationTime;
 
 	std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave?
 
@@ -221,7 +221,7 @@
 	CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; }
 	SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; }
 
-	time_t GetCreationTime() const { return m_creationTime; }
+	mpt::Date::Unix GetCreationTime() const { return m_creationTime; }
 
 // operations
 public:
Index: mptrack/Mptrack.cpp
===================================================================
--- mptrack/Mptrack.cpp	(revision 17411)
+++ mptrack/Mptrack.cpp	(working copy)
@@ -613,6 +613,24 @@
 }
 
 
+CTrackApp::~CTrackApp()
+{
+#if !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) && defined(MPT_LIBCXX_QUIRK_CHRONO_TZ_MEMLEAK)
+	// Work-around memleak (see <https://github.com/microsoft/STL/issues/2504#issuecomment-1068008937>)
+	try
+	{
+		std::chrono::get_tzdb_list().~tzdb_list();
+	} catch(const std::exception &)
+	{
+		// nothing
+	} catch(...)
+	{
+		// nothing
+	}
+#endif
+}
+
+
 class OpenMPTDataRecoveryHandler
 	: public CDataRecoveryHandler
 {
Index: mptrack/Mptrack.h
===================================================================
--- mptrack/Mptrack.h	(revision 17411)
+++ mptrack/Mptrack.h	(working copy)
@@ -166,6 +166,7 @@
 
 public:
 	CTrackApp();
+	~CTrackApp();
 
 	CDataRecoveryHandler *GetDataRecoveryHandler() override;
 	void AddToRecentFileList(LPCTSTR lpszPathName) override;
Index: mptrack/OPLExport.cpp
===================================================================
--- mptrack/OPLExport.cpp	(revision 17411)
+++ mptrack/OPLExport.cpp	(working copy)
@@ -397,7 +397,7 @@
 		SetDlgItemText(IDC_EDIT2, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.GetTitle()).c_str());
 		SetDlgItemText(IDC_EDIT3, mpt::ToWin(m_sndFile.m_songArtist).c_str());
 		if(!m_sndFile.GetFileHistory().empty())
-			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601().substr(0, 10), U_("-"), U_("/"))).c_str());
+			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601(m_sndFile.GetTimezoneInternal()).substr(0, 10), U_("-"), U_("/"))).c_str());
 		SetDlgItemText(IDC_EDIT5, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.m_songMessage.GetFormatted(SongMessage::leCRLF)).c_str());
 
 		m_locked = false;
Index: soundlib/Load_it.cpp
===================================================================
--- soundlib/Load_it.cpp	(revision 17411)
+++ soundlib/Load_it.cpp	(working copy)
@@ -1278,6 +1278,11 @@
 	m_modFormat.type = (GetType() == MOD_TYPE_MPT) ? U_("mptm") : U_("it");
 	m_modFormat.madeWithTracker = std::move(madeWithTracker);
 	m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::Charset::Windows1252 : mpt::Charset::CP437;
+#ifdef MODPLUG_TRACKER
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_V("1.31.00.13"))) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Local;
+#else
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_V("1.31.00.13"))) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Unspecified;
+#endif
 
 	return true;
 }
@@ -1359,15 +1364,19 @@
 		} else if(pModDoc != nullptr)
 		{
 			// Current ("new") timestamp
-			const time_t creationTime = pModDoc->GetCreationTime();
-			mptHistory.loadDate = mpt::Date::AnyGregorian{};
-			//localtime_s(&loadDate, &creationTime);
-			const tm* const p = localtime(&creationTime);
-			if (p != nullptr)
-				mptHistory.loadDate = mpt::Date::AsGregorian(*p);
-			else
-				sndFile.AddToLog(LogError, U_("Unable to retrieve current time."));
-			mptHistory.openTime = (uint32)(difftime(time(nullptr), creationTime) * HISTORY_TIMER_PRECISION);
+			const mpt::Date::Unix creationTime = pModDoc->GetCreationTime();
+			if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			} else if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(creationTime));
+			} else
+			{
+				// assume UTC
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			}
+			mptHistory.openTime = static_cast<uint32>(mpt::round((mpt::Date::UnixAsSeconds(mpt::Date::UnixNow()) - mpt::Date::UnixAsSeconds(creationTime)) * HISTORY_TIMER_PRECISION));
 #endif // MODPLUG_TRACKER
 		}
 
Index: soundlib/Load_mod.cpp
===================================================================
--- soundlib/Load_mod.cpp	(revision 17411)
+++ soundlib/Load_mod.cpp	(working copy)
@@ -2179,6 +2179,11 @@
 		if(mpt::is_in_range(info.dateMonth, 1, 12) && mpt::is_in_range(info.dateDay, 1, 31) && mpt::is_in_range(info.dateHour, 0, 23)
 		   && mpt::is_in_range(info.dateMinute, 0, 59) && mpt::is_in_range(info.dateSecond, 0, 59))
 		{
+#ifdef MODPLUG_TRACKER
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Local;
+#else
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Unspecified;
+#endif
 			FileHistory mptHistory;
 			mptHistory.loadDate.year = info.dateYear + 1900;
 			mptHistory.loadDate.month = info.dateMonth;
Index: soundlib/Sndfile.cpp
===================================================================
--- soundlib/Sndfile.cpp	(revision 17411)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -54,19 +54,47 @@
 }
 
 
-mpt::ustring FileHistory::AsISO8601() const
+mpt::ustring FileHistory::AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const
 {
-	tm date = mpt::Date::AsTm(loadDate);
 	if(openTime > 0)
 	{
 		// Calculate the date when editing finished.
 		double openSeconds = static_cast<double>(openTime) / HISTORY_TIMER_PRECISION;
-		tm tmpLoadDate = mpt::Date::AsTm(loadDate);
-		int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTCtm(tmpLoadDate));
-		int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
-		date = mpt::Date::UnixAsUTCtm(mpt::Date::UnixFromSeconds(saveDateSinceEpoch));
+		mpt::Date::AnyGregorian tmpLoadDate = loadDate;
+		if (internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsLocal(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			// assume UTC for unspecified timezone when calculating
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch))));
+		}
+	} else
+	{
+		if(internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(loadDate));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(loadDate));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			return mpt::Date::ToShortenedISO8601(loadDate);
+		}
 	}
-	return mpt::Date::ToShortenedISO8601(date);
 }
 
 
@@ -483,8 +511,24 @@
 		InitializeGlobals();
 		m_visitedRows.Initialize(true);
 		m_dwCreatedWithVersion = Version::Current();
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
 	}
 
+#ifdef MODPLUG_TRACKER
+	// convert timestamps to UTC
+	if(m_modFormat.timezone == mpt::Date::LogicalTimezone::Local)
+	{
+		for(auto & fileHistoryEntry : m_FileHistory)
+		{
+			if(fileHistoryEntry.HasValidDate())
+			{
+				fileHistoryEntry.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(fileHistoryEntry.loadDate))));
+			}
+		}
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
+	}
+#endif // MODPLUG_TRACKER
+
 	// Adjust channels
 	const auto muteFlag = GetChannelMuteFlag();
 	for(CHANNELINDEX chn = 0; chn < MAX_BASECHANNELS; chn++)
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 17411)
+++ soundlib/Sndfile.h	(working copy)
@@ -247,7 +247,7 @@
 	// Time the file was open in the editor, in 1/18.2th seconds (frequency of a standard DOS timer, to keep compatibility with Impulse Tracker easy).
 	uint32 openTime = 0;
 	// Return the date as a (possibly truncated if not enough precision is available) ISO 8601 formatted date.
-	mpt::ustring AsISO8601() const;
+	mpt::ustring AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const;
 	// Returns true if the date component is valid. Some formats only store edit time, not edit date.
 	bool HasValidDate() const
 	{
@@ -285,6 +285,7 @@
 	mpt::ustring originalFormatName; // "FastTracker 2" in the case of converted formats like MO3 or GDM
 	mpt::ustring originalType;       // "xm" in the case of converted formats like MO3 or GDM
 	mpt::Charset charset = mpt::Charset::UTF8;
+	mpt::Date::LogicalTimezone timezone = mpt::Date::LogicalTimezone::Unspecified;
 };
 
 
@@ -752,6 +753,10 @@
 			return GetCharsetFile();
 		#endif // MODPLUG_TRACKER
 	}
+	mpt::Date::LogicalTimezone GetTimezoneInternal() const
+	{
+		return m_modFormat.timezone;
+	}
 
 	ModMessageHeuristicOrder GetMessageHeuristic() const;
 
Index: src/mpt/base/detect_quirks.hpp
===================================================================
--- src/mpt/base/detect_quirks.hpp	(revision 17411)
+++ src/mpt/base/detect_quirks.hpp	(working copy)
@@ -215,14 +215,15 @@
 #elif MPT_LIBCXX_LLVM_BEFORE(7000)
 #define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
 #endif
-#if MPT_LIBCXX_MS && (MPT_MSVC_BEFORE(2022, 2) || !MPT_COMPILER_MSVC)
+#if MPT_LIBCXX_MS && (MPT_MSVC_BEFORE(2022, 3) || !MPT_COMPILER_MSVC)
 // Causes massive memory leaks.
 // See
 // <https://developercommunity.visualstudio.com/t/stdchronoget-tzdb-list-memory-leak/1644641>
 // / <https://github.com/microsoft/STL/issues/2504>.
+#define MPT_LIBCXX_QUIRK_CHRONO_TZ_MEMLEAK
+#endif
+#if MPT_LIBCXX_GNU
 #define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
-#elif MPT_LIBCXX_GNU
-#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
 #endif
 #endif
 
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 17411)
+++ test/test.cpp	(working copy)
@@ -2071,12 +2071,25 @@
 	// Edit history
 	VERIFY_EQUAL_NONCONT(sndFile.GetFileHistory().size() > 15, true);
 	const FileHistory &fh = sndFile.GetFileHistory().front();
-	VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
+#ifdef MODPLUG_TRACKER
+	if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+	{
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).year, 2011);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).month, 6);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).day, 14);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 21);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).minutes, 8);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).seconds, 32);
+	} else
+#endif // MODPLUG_TRACKER
+	{
+		VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
+	}
 	VERIFY_EQUAL_NONCONT((uint32)((double)fh.openTime / HISTORY_TIMER_PRECISION), 31);
 
 	// Macros
chrono-v13.patch (26,451 bytes)   
manx

manx

2022-06-06 08:01

administrator   ~0005197

[Mod] Edit History: Dates in Edit History can now have 1 of 3 timezone designators: UTC, Local, Unspecified.
[Mod] Edit History: When the timezone is Unspecified, dates are now printed without the UTC timezone signifier 'Z' in ISO8601 format.
[Mod] IT/MPTM: Dates are now written in UTC. Loading decides by looking at m_dwLastSavedWithVersion how to interpret dates. After loading, all dates with Local timezone are converted into UTC timezone, dates with Unspecified timezone are kept as is.
[Mod] MOD: Assume dates are in Local timezone.
[Imp] When calculating save time from load time and open duration, properly take Local timezone into account.
[Ref] Edit History: OpenMPT still displays dates in Local timezone, and assumes so also for Unspecified timezone dates.
[Fix] Work-around MSVC tzdb memleak. See &lt;https://github.com/microsoft/STL/issues/2504#issuecomment-1068008937>.
[Reg/Fix] On pre-C++20 builds, Local time stored during DST will now be interpreted as non-DST. This is a fundamental limitation of the C time.h API that does not allow to determine whether a specific Local time has DST active or not from the time stamp alone (or whether it is ambiguous). As no module format stores the isdst flag of struct tm, this information has always been always lost. Timestamp + duration calculation have always been wrong here when crossing DST change. The alternative to this regression/misinterpretation would be to always do it wrong forever.
[Ref] mptTime: Remove all &lt;ctime> usage for modern builds.
chrono-v14.patch (27,445 bytes)   
Index: build/pch/PCH.h
===================================================================
--- build/pch/PCH.h	(revision 17419)
+++ build/pch/PCH.h	(working copy)
@@ -151,7 +151,6 @@
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
-#include <time.h>
 
 
 #endif // MPT_BUILD_ENABLE_PCH
Index: common/mptTime.cpp
===================================================================
--- common/mptTime.cpp	(revision 17419)
+++ common/mptTime.cpp	(working copy)
@@ -83,7 +83,7 @@
 
 #endif // MODPLUG_TRACKER
 
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) || defined(MPT_TIME_CTIME)
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 static int32 ToDaynum(int32 year, int32 month, int32 day)
 {
@@ -115,10 +115,6 @@
 	day = static_cast<int32>(dd);
 }
 
-#endif
-
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
-
 mpt::Date::Unix UnixFromUTC(UTC timeUtc)
 {
 	int32 daynum = ToDaynum(timeUtc.year, timeUtc.month, timeUtc.day);
@@ -140,10 +136,45 @@
 	result.day = day;
 	result.hours = static_cast<int32>(hours);
 	result.minutes = static_cast<int32>(minutes);
-	result.seconds = static_cast<int64>(seconds);
+	result.seconds = seconds;
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+mpt::Date::Unix UnixFromLocal(Local timeLocal)
+{
+	std::tm tmp{};
+	tmp.tm_year = timeLocal.year - 1900;
+	tmp.tm_mon = timeLocal.month - 1;
+	tmp.tm_mday = timeLocal.day;
+	tmp.tm_hour = timeLocal.hours;
+	tmp.tm_min = timeLocal.minutes;
+	tmp.tm_sec = static_cast<int>(timeLocal.seconds);
+	return mpt::Date::UnixFromSeconds(static_cast<int64>(std::mktime(&tmp)));
+}
+
+mpt::Date::Local UnixAsLocal(Unix tp)
+{
+	std::time_t time_tp = static_cast<std::time_t>(mpt::Date::UnixAsSeconds(tp));
+	std::tm *tmp = std::localtime(&time_tp);
+	if(!tmp)
+	{
+		return mpt::Date::Local{};
+	}
+	std::tm local = *tmp;
+	mpt::Date::Local result{};
+	result.year = local.tm_year + 1900;
+	result.month = local.tm_mon + 1;
+	result.day = local.tm_mday;
+	result.hours = local.tm_hour;
+	result.minutes = local.tm_min;
+	result.seconds = local.tm_sec;
+	return result;
+}
+
+#endif // MODPLUG_TRACKER
+
 #endif
 
 template <LogicalTimezone TZ>
@@ -193,85 +224,13 @@
 	return ToShortenedISO8601Impl(date);
 }
 
-#if defined(MPT_TIME_CTIME)
-
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc)
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date)
 {
-	int32 daynum = ToDaynum(timeUtc.tm_year+1900, timeUtc.tm_mon+1, timeUtc.tm_mday);
-	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970,1,1))*24*60*60 + timeUtc.tm_hour*60*60 + timeUtc.tm_min*60 + timeUtc.tm_sec;
-	return mpt::Date::UnixFromSeconds(seconds);
+	return ToShortenedISO8601Impl(date);
 }
+#endif // MODPLUG_TRACKER
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime)
-{
-	int64 tmp = mpt::Date::UnixAsSeconds(unixtime);
-	int64 seconds = tmp % 60; tmp /= 60;
-	int64 minutes = tmp % 60; tmp /= 60;
-	int64 hours   = tmp % 24; tmp /= 24;
-	int32 year = 0, month = 0, day = 0;
-	FromDaynum(static_cast<int32>(tmp) + ToDaynum(1970,1,1), year, month, day);
-	tm result = {};
-	result.tm_year = year - 1900;
-	result.tm_mon = month - 1;
-	result.tm_mday = day;
-	result.tm_hour = static_cast<int32>(hours);
-	result.tm_min = static_cast<int32>(minutes);
-	result.tm_sec = static_cast<int32>(seconds);
-	return result;
-}
-
-mpt::ustring ToShortenedISO8601(tm date)
-{
-	// We assume date in UTC here.
-	// There are too many differences in supported format specifiers in strftime()
-	// and strftime does not support reduced precision ISO8601 at all.
-	// Just do the formatting ourselves.
-	mpt::ustring result;
-	mpt::ustring tz = U_("Z");
-	if(date.tm_year == 0)
-	{
-		return result;
-	}
-	result += mpt::ufmt::dec0<4>(date.tm_year + 1900);
-	if(date.tm_mon < 0 || date.tm_mon > 11)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mon + 1);
-	if(date.tm_mday < 1 || date.tm_mday > 31)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mday);
-	if(date.tm_hour == 0 && date.tm_min == 0 && date.tm_sec == 0)
-	{
-		return result;
-	}
-	if(date.tm_hour < 0 || date.tm_hour > 23)
-	{
-		return result;
-	}
-	if(date.tm_min < 0 || date.tm_min > 59)
-	{
-		return result;
-	}
-	result += U_("T");
-	if(date.tm_isdst > 0)
-	{
-		tz = U_("+01:00");
-	}
-	result += mpt::ufmt::dec0<2>(date.tm_hour) + U_(":") + mpt::ufmt::dec0<2>(date.tm_min);
-	if(date.tm_sec < 0 || date.tm_sec > 61)
-	{
-		return result + tz;
-	}
-	result += U_(":") + mpt::ufmt::dec0<2>(date.tm_sec);
-	result += tz;
-	return result;
-}
-
-#endif
-
 } // namespace Date
 } // namespace mpt
 
Index: common/mptTime.h
===================================================================
--- common/mptTime.h	(revision 17419)
+++ common/mptTime.h	(working copy)
@@ -18,16 +18,10 @@
 #include <string>
 
 #if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
-#include <time.h>
+#include <ctime>
 #endif
 
-#define MPT_TIME_CTIME
 
-#if defined(MPT_TIME_CTIME)
-#include <time.h>
-#endif
-
-
 OPENMPT_NAMESPACE_BEGIN
 
 
@@ -99,37 +93,38 @@
 
 using AnyGregorian = Gregorian<LogicalTimezone::Unspecified>;
 
-#if defined(MPT_TIME_CTIME)
-inline tm AsTm(AnyGregorian val)
+using UTC = Gregorian<LogicalTimezone::UTC>;
+
+#if defined(MODPLUG_TRACKER)
+using Local = Gregorian<LogicalTimezone::Local>;
+#endif // MODPLUG_TRACKER
+
+template <LogicalTimezone TZ>
+inline Gregorian<TZ> interpret_as_timezone(AnyGregorian gregorian)
 {
-	tm result{};
-	result.tm_year = val.year - 1900;
-	result.tm_mon = val.month - 1;
-	result.tm_mday = val.day;
-	result.tm_hour = val.hours;
-	result.tm_min = val.minutes;
-	result.tm_sec = static_cast<int>(val.seconds);
+	Gregorian<TZ> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-inline AnyGregorian AsGregorian(tm val)
+
+template <LogicalTimezone TZ>
+inline Gregorian<LogicalTimezone::Unspecified> forget_timezone(Gregorian<TZ> gregorian)
 {
-	AnyGregorian result{};
-	result.year = val.tm_year + 1900;
-	result.month = val.tm_mon + 1;
-	result.day = val.tm_mday;
-	result.hours = val.tm_hour;
-	result.minutes = val.tm_min;
-	result.seconds = val.tm_sec;
+	Gregorian<LogicalTimezone::Unspecified> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-#endif
 
-using UTC = Gregorian<LogicalTimezone::UTC>;
-
-#if defined(MODPLUG_TRACKER)
-using Local = Gregorian<LogicalTimezone::Local>;
-#endif // MODPLUG_TRACKER
-
 #if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 using Unix = std::chrono::system_clock::time_point;
@@ -151,15 +146,15 @@
 
 inline mpt::Date::Unix UnixFromUTC(UTC utc)
 {
-	std::chrono::year_month_day ymd =
-		std::chrono::year{utc.year} /
-		std::chrono::month{utc.month} /
-		std::chrono::day{utc.day};
-	std::chrono::hh_mm_ss<std::chrono::seconds> hms{
-		std::chrono::hours{utc.hours} +
-		std::chrono::minutes{utc.minutes} +
-		std::chrono::seconds{utc.seconds}};
-	return std::chrono::system_clock::time_point{static_cast<std::chrono::sys_days>(ymd)} + hms.to_duration();
+	return std::chrono::system_clock::time_point{
+		std::chrono::sys_days {
+			std::chrono::year{ utc.year } /
+			std::chrono::month{ utc.month } /
+			std::chrono::day{ utc.day }
+		} +
+		std::chrono::hours{ utc.hours } +
+		std::chrono::minutes{ utc.minutes } +
+		std::chrono::seconds{ utc.seconds }};
 }
 
 inline mpt::Date::UTC UnixAsUTC(Unix tp)
@@ -177,6 +172,40 @@
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+inline mpt::Date::Unix UnixFromLocal(Local local)
+{
+	std::chrono::time_point<std::chrono::local_t, std::chrono::seconds> local_tp = 
+		std::chrono::local_days {
+			std::chrono::year{ local.year } /
+			std::chrono::month{ local.month } /
+			std::chrono::day{ local.day }
+		} +
+		std::chrono::hours{ local.hours } +
+		std::chrono::minutes{ local.minutes } +
+		std::chrono::seconds{ local.seconds };
+	return std::chrono::zoned_time{std::chrono::current_zone(), local_tp}.get_sys_time();
+}
+
+inline mpt::Date::Local UnixAsLocal(Unix tp)
+{
+	std::chrono::zoned_time local_tp{ std::chrono::current_zone(), tp };
+	std::chrono::local_days dp = std::chrono::floor<std::chrono::days>(local_tp.get_local_time());
+	std::chrono::year_month_day ymd{dp};
+	std::chrono::hh_mm_ss hms{local_tp.get_local_time() - dp};
+	mpt::Date::Local result;
+	result.year = static_cast<int>(ymd.year());
+	result.month = static_cast<unsigned int>(ymd.month());
+	result.day = static_cast<unsigned int>(ymd.day());
+	result.hours = hms.hours().count();
+	result.minutes = hms.minutes().count();
+	result.seconds = hms.seconds().count();
+	return result;
+}
+
+#endif // MODPLUG_TRACKER
+
 #else
 
 // int64 counts 1s since 1970-01-01T00:00Z
@@ -195,7 +224,7 @@
 
 inline Unix UnixNow()
 {
-	return Unix{static_cast<int64>(time(nullptr))};
+	return Unix{static_cast<int64>(std::time(nullptr))};
 }
 
 inline int64 UnixAsSeconds(Unix tp)
@@ -212,21 +241,23 @@
 
 mpt::Date::UTC UnixAsUTC(Unix tp);
 
-#endif
+#if defined(MODPLUG_TRACKER)
 
-mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
+mpt::Date::Unix UnixFromLocal(Local timeLocal);
 
-mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
+mpt::Date::Local UnixAsLocal(Unix tp);
 
-#if defined(MPT_TIME_CTIME)
+#endif // MODPLUG_TRACKER
 
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc);
+#endif
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime);
+mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
 
-mpt::ustring ToShortenedISO8601(tm date); // i.e. 2015-01-15T18:32:01Z
+mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
 
-#endif
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date); // i.e. 2015-01-15T18:32:01
+#endif // MODPLUG_TRACKER
 
 } // namespace Date
 } // namespace mpt
Index: common/versionNumber.h
===================================================================
--- common/versionNumber.h	(revision 17419)
+++ common/versionNumber.h	(working copy)
@@ -18,6 +18,6 @@
 #define VER_MAJORMAJOR  1
 #define VER_MAJOR      31
 #define VER_MINOR      00
-#define VER_MINORMINOR 12
+#define VER_MINORMINOR 13
 
 OPENMPT_NAMESPACE_END
Index: libopenmpt/libopenmpt_impl.cpp
===================================================================
--- libopenmpt/libopenmpt_impl.cpp	(revision 17419)
+++ libopenmpt/libopenmpt_impl.cpp	(working copy)
@@ -1241,7 +1241,7 @@
 		if ( m_sndFile->GetFileHistory().empty() || !m_sndFile->GetFileHistory().back().HasValidDate() ) {
 			return std::string();
 		}
-		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601() );
+		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601( m_sndFile->GetTimezoneInternal() ) );
 	} else if ( key == std::string("message") ) {
 		std::string retval = m_sndFile->m_songMessage.GetFormatted( OpenMPT::SongMessage::leLF );
 		if ( retval.empty() ) {
Index: mptrack/dlg_misc.cpp
===================================================================
--- mptrack/dlg_misc.cpp	(revision 17419)
+++ mptrack/dlg_misc.cpp	(working copy)
@@ -1311,18 +1311,15 @@
 	for(const auto &entry : editHistory)
 	{
 		totalTime += entry.openTime;
-
 		// Date
-		CString sDate;
+		CString sDate = CString(_T("<unknown date>"));
 		if(entry.HasValidDate())
 		{
-			TCHAR szDate[32];
-			const tm loadDate = mpt::Date::AsTm(entry.loadDate);
-			_tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &loadDate);
-			sDate = szDate;
-		} else
-		{
-			sDate = _T("<unknown date>");
+			const mpt::Date::Unix unixdate = ((m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local) || (m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Unspecified))
+				? mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(entry.loadDate))
+				: mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(entry.loadDate));
+				;
+			sDate = CTime(mpt::Date::UnixAsSeconds(unixdate)).Format(_T("%d %b %Y, %H:%M:%S"));
 		}
 		// Time + stuff
 		uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION);
Index: mptrack/Moddoc.cpp
===================================================================
--- mptrack/Moddoc.cpp	(revision 17419)
+++ mptrack/Moddoc.cpp	(working copy)
@@ -133,7 +133,7 @@
 	, m_InstrumentUndo(*this)
 {
 	// Set the creation date of this file (or the load time if we're loading an existing file)
-	time(&m_creationTime);
+	m_creationTime = mpt::Date::UnixNow();
 
 	ReinitRecordState();
 
Index: mptrack/Moddoc.h
===================================================================
--- mptrack/Moddoc.h	(revision 17419)
+++ mptrack/Moddoc.h	(working copy)
@@ -14,10 +14,10 @@
 
 #include "Sndfile.h"
 #include "../common/misc_util.h"
+#include "../common/mptTime.h"
 #include "Undo.h"
 #include "Notification.h"
 #include "UpdateHints.h"
-#include <time.h>
 
 OPENMPT_NAMESPACE_BEGIN
 
@@ -130,7 +130,7 @@
 	CSampleUndo m_SampleUndo;
 	CInstrumentUndo m_InstrumentUndo;
 	SplitKeyboardSettings m_SplitKeyboardSettings;	// this is maybe not the best place to keep them, but it should do the job
-	time_t m_creationTime;
+	mpt::Date::Unix m_creationTime;
 
 	std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave?
 
@@ -221,7 +221,7 @@
 	CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; }
 	SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; }
 
-	time_t GetCreationTime() const { return m_creationTime; }
+	mpt::Date::Unix GetCreationTime() const { return m_creationTime; }
 
 // operations
 public:
Index: mptrack/Mptrack.cpp
===================================================================
--- mptrack/Mptrack.cpp	(revision 17419)
+++ mptrack/Mptrack.cpp	(working copy)
@@ -613,6 +613,24 @@
 }
 
 
+CTrackApp::~CTrackApp()
+{
+#if !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) && defined(MPT_LIBCXX_QUIRK_CHRONO_TZ_MEMLEAK)
+	// Work-around memleak (see <https://github.com/microsoft/STL/issues/2504#issuecomment-1068008937>)
+	try
+	{
+		std::chrono::get_tzdb_list().~tzdb_list();
+	} catch(const std::exception &)
+	{
+		// nothing
+	} catch(...)
+	{
+		// nothing
+	}
+#endif
+}
+
+
 class OpenMPTDataRecoveryHandler
 	: public CDataRecoveryHandler
 {
Index: mptrack/Mptrack.h
===================================================================
--- mptrack/Mptrack.h	(revision 17419)
+++ mptrack/Mptrack.h	(working copy)
@@ -166,6 +166,7 @@
 
 public:
 	CTrackApp();
+	~CTrackApp();
 
 	CDataRecoveryHandler *GetDataRecoveryHandler() override;
 	void AddToRecentFileList(LPCTSTR lpszPathName) override;
Index: mptrack/OPLExport.cpp
===================================================================
--- mptrack/OPLExport.cpp	(revision 17419)
+++ mptrack/OPLExport.cpp	(working copy)
@@ -397,7 +397,7 @@
 		SetDlgItemText(IDC_EDIT2, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.GetTitle()).c_str());
 		SetDlgItemText(IDC_EDIT3, mpt::ToWin(m_sndFile.m_songArtist).c_str());
 		if(!m_sndFile.GetFileHistory().empty())
-			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601().substr(0, 10), U_("-"), U_("/"))).c_str());
+			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601(m_sndFile.GetTimezoneInternal()).substr(0, 10), U_("-"), U_("/"))).c_str());
 		SetDlgItemText(IDC_EDIT5, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.m_songMessage.GetFormatted(SongMessage::leCRLF)).c_str());
 
 		m_locked = false;
Index: soundlib/Load_it.cpp
===================================================================
--- soundlib/Load_it.cpp	(revision 17419)
+++ soundlib/Load_it.cpp	(working copy)
@@ -1278,6 +1278,11 @@
 	m_modFormat.type = (GetType() == MOD_TYPE_MPT) ? U_("mptm") : U_("it");
 	m_modFormat.madeWithTracker = std::move(madeWithTracker);
 	m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::Charset::Windows1252 : mpt::Charset::CP437;
+#ifdef MODPLUG_TRACKER
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_V("1.31.00.13"))) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Local;
+#else
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_V("1.31.00.13"))) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Unspecified;
+#endif
 
 	return true;
 }
@@ -1359,15 +1364,19 @@
 		} else if(pModDoc != nullptr)
 		{
 			// Current ("new") timestamp
-			const time_t creationTime = pModDoc->GetCreationTime();
-			mptHistory.loadDate = mpt::Date::AnyGregorian{};
-			//localtime_s(&loadDate, &creationTime);
-			const tm* const p = localtime(&creationTime);
-			if (p != nullptr)
-				mptHistory.loadDate = mpt::Date::AsGregorian(*p);
-			else
-				sndFile.AddToLog(LogError, U_("Unable to retrieve current time."));
-			mptHistory.openTime = (uint32)(difftime(time(nullptr), creationTime) * HISTORY_TIMER_PRECISION);
+			const mpt::Date::Unix creationTime = pModDoc->GetCreationTime();
+			if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			} else if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(creationTime));
+			} else
+			{
+				// assume UTC
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			}
+			mptHistory.openTime = static_cast<uint32>(mpt::round((mpt::Date::UnixAsSeconds(mpt::Date::UnixNow()) - mpt::Date::UnixAsSeconds(creationTime)) * HISTORY_TIMER_PRECISION));
 #endif // MODPLUG_TRACKER
 		}
 
Index: soundlib/Load_mod.cpp
===================================================================
--- soundlib/Load_mod.cpp	(revision 17419)
+++ soundlib/Load_mod.cpp	(working copy)
@@ -2179,6 +2179,11 @@
 		if(mpt::is_in_range(info.dateMonth, 1, 12) && mpt::is_in_range(info.dateDay, 1, 31) && mpt::is_in_range(info.dateHour, 0, 23)
 		   && mpt::is_in_range(info.dateMinute, 0, 59) && mpt::is_in_range(info.dateSecond, 0, 59))
 		{
+#ifdef MODPLUG_TRACKER
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Local;
+#else
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Unspecified;
+#endif
 			FileHistory mptHistory;
 			mptHistory.loadDate.year = info.dateYear + 1900;
 			mptHistory.loadDate.month = info.dateMonth;
Index: soundlib/Sndfile.cpp
===================================================================
--- soundlib/Sndfile.cpp	(revision 17419)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -54,19 +54,47 @@
 }
 
 
-mpt::ustring FileHistory::AsISO8601() const
+mpt::ustring FileHistory::AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const
 {
-	tm date = mpt::Date::AsTm(loadDate);
 	if(openTime > 0)
 	{
 		// Calculate the date when editing finished.
 		double openSeconds = static_cast<double>(openTime) / HISTORY_TIMER_PRECISION;
-		tm tmpLoadDate = mpt::Date::AsTm(loadDate);
-		int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTCtm(tmpLoadDate));
-		int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
-		date = mpt::Date::UnixAsUTCtm(mpt::Date::UnixFromSeconds(saveDateSinceEpoch));
+		mpt::Date::AnyGregorian tmpLoadDate = loadDate;
+		if (internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsLocal(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			// assume UTC for unspecified timezone when calculating
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch))));
+		}
+	} else
+	{
+		if(internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(loadDate));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(loadDate));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			return mpt::Date::ToShortenedISO8601(loadDate);
+		}
 	}
-	return mpt::Date::ToShortenedISO8601(date);
 }
 
 
@@ -483,8 +511,24 @@
 		InitializeGlobals();
 		m_visitedRows.Initialize(true);
 		m_dwCreatedWithVersion = Version::Current();
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
 	}
 
+#ifdef MODPLUG_TRACKER
+	// convert timestamps to UTC
+	if(m_modFormat.timezone == mpt::Date::LogicalTimezone::Local)
+	{
+		for(auto & fileHistoryEntry : m_FileHistory)
+		{
+			if(fileHistoryEntry.HasValidDate())
+			{
+				fileHistoryEntry.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(fileHistoryEntry.loadDate))));
+			}
+		}
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
+	}
+#endif // MODPLUG_TRACKER
+
 	// Adjust channels
 	const auto muteFlag = GetChannelMuteFlag();
 	for(CHANNELINDEX chn = 0; chn < MAX_BASECHANNELS; chn++)
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 17419)
+++ soundlib/Sndfile.h	(working copy)
@@ -247,7 +247,7 @@
 	// Time the file was open in the editor, in 1/18.2th seconds (frequency of a standard DOS timer, to keep compatibility with Impulse Tracker easy).
 	uint32 openTime = 0;
 	// Return the date as a (possibly truncated if not enough precision is available) ISO 8601 formatted date.
-	mpt::ustring AsISO8601() const;
+	mpt::ustring AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const;
 	// Returns true if the date component is valid. Some formats only store edit time, not edit date.
 	bool HasValidDate() const
 	{
@@ -285,6 +285,7 @@
 	mpt::ustring originalFormatName; // "FastTracker 2" in the case of converted formats like MO3 or GDM
 	mpt::ustring originalType;       // "xm" in the case of converted formats like MO3 or GDM
 	mpt::Charset charset = mpt::Charset::UTF8;
+	mpt::Date::LogicalTimezone timezone = mpt::Date::LogicalTimezone::Unspecified;
 };
 
 
@@ -752,6 +753,10 @@
 			return GetCharsetFile();
 		#endif // MODPLUG_TRACKER
 	}
+	mpt::Date::LogicalTimezone GetTimezoneInternal() const
+	{
+		return m_modFormat.timezone;
+	}
 
 	ModMessageHeuristicOrder GetMessageHeuristic() const;
 
Index: src/mpt/base/detect_quirks.hpp
===================================================================
--- src/mpt/base/detect_quirks.hpp	(revision 17419)
+++ src/mpt/base/detect_quirks.hpp	(working copy)
@@ -235,13 +235,15 @@
 #define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
 #endif
 #if MPT_LIBCXX_MS && (MPT_MSVC_BEFORE(2022, 2) || !MPT_COMPILER_MSVC)
+#elif MPT_LIBCXX_GNU
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#endif
+#if MPT_LIBCXX_MS && (MPT_MSVC_BEFORE(2022, 3) || !MPT_COMPILER_MSVC)
 // Causes massive memory leaks.
 // See
 // <https://developercommunity.visualstudio.com/t/stdchronoget-tzdb-list-memory-leak/1644641>
 // / <https://github.com/microsoft/STL/issues/2504>.
-#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
-#elif MPT_LIBCXX_GNU
-#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#define MPT_LIBCXX_QUIRK_CHRONO_TZ_MEMLEAK
 #endif
 #endif
 
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 17419)
+++ test/test.cpp	(working copy)
@@ -2096,12 +2096,29 @@
 	// Edit history
 	VERIFY_EQUAL_NONCONT(sndFile.GetFileHistory().size() > 15, true);
 	const FileHistory &fh = sndFile.GetFileHistory().front();
-	VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
+#ifdef MODPLUG_TRACKER
+	if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+	{
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).year, 2011);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).month, 6);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).day, 14);
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 22);
+#else
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 21);
+#endif
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).minutes, 8);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).seconds, 32);
+	} else
+#endif // MODPLUG_TRACKER
+	{
+		VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
+	}
 	VERIFY_EQUAL_NONCONT((uint32)((double)fh.openTime / HISTORY_TIMER_PRECISION), 31);
 
 	// Macros
chrono-v14.patch (27,445 bytes)   
manx

manx

2022-06-07 08:03

administrator   ~0005198

Last edited: 2022-06-07 08:20

[Mod] Edit History: Dates in Edit History can now have 1 of 3 timezone designators: UTC, Local, Unspecified.
[Mod] Edit History: When the timezone is Unspecified, dates are now formatted without the UTC timezone signifier 'Z' in ISO8601 format. This affects libopenmpt.
[Mod] IT/MPTM: Dates are now written in UTC. Loading decides by looking at m_dwLastSavedWithVersion how to interpret dates. After loading, all dates with Local timezone are converted into UTC timezone, dates with Unspecified timezone are kept as is.
[Mod] MOD: Assume dates are in Local timezone.
[Ref] Edit History: OpenMPT still displays dates in Local timezone, and assumes so also for Unspecified timezone dates.
[Imp] When calculating save time from load time and open duration, properly take Local timezone into account. This uses C++20 tzdb (for C++20 or later), or Windows historic timezone information (for Windows 8 or later), or current Windows timezone information (for Windows XP or later), or C stdlib implementation (otherwise).
[Reg/Fix] On pre-C++20 pre-WinXP builds, Local time stored during DST will now be interpreted as non-DST. This is a fundamental limitation of the C time.h API that does not allow to determine whether a specific Local time has DST active or not from the timestamp alone (or whether it is ambiguous). As no module format stores the isdst flag of struct tm, this information has always been always lost. Timestamp + duration calculation have always been wrong here when crossing DST change. The alternative to this regression/misinterpretation would be to always do it wrong forever.
[Fix] Work-around MSVC tzdb memleak. See &lt;https://github.com/microsoft/STL/issues/2504#issuecomment-1068008937>.
[Ref] mptTime: Remove all &lt;ctime> usage for modern builds.
chrono-v15.patch (32,627 bytes)   
Index: build/pch/PCH.h
===================================================================
--- build/pch/PCH.h	(revision 17427)
+++ build/pch/PCH.h	(working copy)
@@ -151,7 +151,6 @@
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
-#include <time.h>
 
 
 #endif // MPT_BUILD_ENABLE_PCH
Index: common/mptTime.cpp
===================================================================
--- common/mptTime.cpp	(revision 17427)
+++ common/mptTime.cpp	(working copy)
@@ -83,7 +83,7 @@
 
 #endif // MODPLUG_TRACKER
 
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) || defined(MPT_TIME_CTIME)
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 static int32 ToDaynum(int32 year, int32 month, int32 day)
 {
@@ -115,10 +115,6 @@
 	day = static_cast<int32>(dd);
 }
 
-#endif
-
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
-
 mpt::Date::Unix UnixFromUTC(UTC timeUtc)
 {
 	int32 daynum = ToDaynum(timeUtc.year, timeUtc.month, timeUtc.day);
@@ -140,12 +136,155 @@
 	result.day = day;
 	result.hours = static_cast<int32>(hours);
 	result.minutes = static_cast<int32>(minutes);
-	result.seconds = static_cast<int64>(seconds);
+	result.seconds = seconds;
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+mpt::Date::Unix UnixFromLocal(Local timeLocal)
+{
+#if defined(MPT_FALLBACK_TIMEZONE_WINDOWS_HISTORIC)
+	SYSTEMTIME sys_local{};
+	sys_local.wYear = static_cast<uint16>(timeLocal.year);
+	sys_local.wMonth = static_cast<uint16>(timeLocal.month);
+	sys_local.wDay = static_cast<uint16>(timeLocal.day);
+	sys_local.wHour = static_cast<uint16>(timeLocal.hours);
+	sys_local.wMinute = static_cast<uint16>(timeLocal.minutes);
+	sys_local.wSecond = static_cast<uint16>(timeLocal.seconds);
+	sys_local.wMilliseconds = 0;
+	DYNAMIC_TIME_ZONE_INFORMATION dtzi{};
+	if(GetDynamicTimeZoneInformation(&dtzi) == TIME_ZONE_ID_INVALID) // WinVista
+	{
+		return mpt::Date::Unix{};
+	}
+	SYSTEMTIME sys_utc{};
+	if(TzSpecificLocalTimeToSystemTimeEx(&dzti, &sys_local, &sys_utc) == FALSE) // Win7/Win8
+	{
+		return mpt::Date::Unix{};
+	}
+	FILETIME ft{};
+	if(SystemTimeToFileTime(&sys_utc, &ft) == FALSE) // Win 2000
+	{
+		return mpt::Date::Unix{};
+	}
+	ULARGE_INTEGER time_value{};
+	time_value.LowPart = ft.dwLowDateTime;
+	time_value.HighPart = ft.dwHighDateTime;
+	return mpt::Date::UnixFromSeconds(static_cast<int64>((time_value.QuadPart - 116444736000000000LL) / 10000000LL));
+#elif defined(MPT_FALLBACK_TIMEZONE_WINDOWS_CURRENT)
+	SYSTEMTIME sys_local{};
+	sys_local.wYear = static_cast<uint16>(timeLocal.year);
+	sys_local.wMonth = static_cast<uint16>(timeLocal.month);
+	sys_local.wDay = static_cast<uint16>(timeLocal.day);
+	sys_local.wHour = static_cast<uint16>(timeLocal.hours);
+	sys_local.wMinute = static_cast<uint16>(timeLocal.minutes);
+	sys_local.wSecond = static_cast<uint16>(timeLocal.seconds);
+	sys_local.wMilliseconds = 0;
+	SYSTEMTIME sys_utc{};
+	if(TzSpecificLocalTimeToSystemTime(NULL, &sys_local, &sys_utc) == FALSE) // WinXP
+	{
+		return mpt::Date::Unix{};
+	}
+	FILETIME ft{};
+	if(SystemTimeToFileTime(&sys_utc, &ft) == FALSE) // Win 2000
+	{
+		return mpt::Date::Unix{};
+	}
+	ULARGE_INTEGER time_value{};
+	time_value.LowPart = ft.dwLowDateTime;
+	time_value.HighPart = ft.dwHighDateTime;
+	return mpt::Date::UnixFromSeconds(static_cast<int64>((time_value.QuadPart - 116444736000000000LL) / 10000000LL));
+#elif defined(MPT_FALLBACK_TIMEZONE_C)
+	std::tm tmp{};
+	tmp.tm_year = timeLocal.year - 1900;
+	tmp.tm_mon = timeLocal.month - 1;
+	tmp.tm_mday = timeLocal.day;
+	tmp.tm_hour = timeLocal.hours;
+	tmp.tm_min = timeLocal.minutes;
+	tmp.tm_sec = static_cast<int>(timeLocal.seconds);
+	return mpt::Date::UnixFromSeconds(static_cast<int64>(std::mktime(&tmp)));
 #endif
+}
 
+mpt::Date::Local UnixAsLocal(Unix tp)
+{
+#if defined(MPT_FALLBACK_TIMEZONE_WINDOWS_HISTORIC)
+	ULARGE_INTEGER time_value{};
+	time_value.QuadPart = static_cast<int64>(mpt::Date::UnixAsSeconds(tp)) * 10000000LL + 116444736000000000LL;
+	FILETIME ft{};
+	ft.dwLowDateTime = time_value.LowPart;
+	ft.dwHighDateTime = time_value.HighPart;
+	SYSTEMTIME sys_utc{};
+	if(FileTimeToSystemTime(&ft, &sys_utc) == FALSE) // WinXP
+	{
+		return mpt::Date::Local{};
+	}
+	DYNAMIC_TIME_ZONE_INFORMATION dtzi{};
+	if(GetDynamicTimeZoneInformation(&dtzi) == TIME_ZONE_ID_INVALID) // WinVista
+	{
+		return mpt::Date::Local{};
+	}
+	SYSTEMTIME sys_local{};
+	if(SystemTimeToTzSpecificLocalTimeEx(&dzti, &sys_utc, &sys_local) == FALSE) // Win7/Win8
+	{
+		return mpt::Date::Local{};
+	}
+	mpt::Date::Local result{};
+	result.year = sys_local.wYear;
+	result.month = sys_local.wMonth;
+	result.day = sys_local.wDay;
+	result.hours = sys_local.wHour;
+	result.minutes = sys_local.wMinute;
+	result.seconds = sys_local.wSecond;
+	return result;
+#elif defined(MPT_FALLBACK_TIMEZONE_WINDOWS_CURRENT)
+	ULARGE_INTEGER time_value{};
+	time_value.QuadPart = static_cast<int64>(mpt::Date::UnixAsSeconds(tp)) * 10000000LL + 116444736000000000LL;
+	FILETIME ft{};
+	ft.dwLowDateTime = time_value.LowPart;
+	ft.dwHighDateTime = time_value.HighPart;
+	SYSTEMTIME sys_utc{};
+	if(FileTimeToSystemTime(&ft, &sys_utc) == FALSE) // WinXP
+	{
+		return mpt::Date::Local{};
+	}
+	SYSTEMTIME sys_local{};
+	if(SystemTimeToTzSpecificLocalTime(NULL, &sys_utc, &sys_local) == FALSE) // Win2000
+	{
+		return mpt::Date::Local{};
+	}
+	mpt::Date::Local result{};
+	result.year = sys_local.wYear;
+	result.month = sys_local.wMonth;
+	result.day = sys_local.wDay;
+	result.hours = sys_local.wHour;
+	result.minutes = sys_local.wMinute;
+	result.seconds = sys_local.wSecond;
+	return result;
+#elif defined(MPT_FALLBACK_TIMEZONE_C)
+	std::time_t time_tp = static_cast<std::time_t>(mpt::Date::UnixAsSeconds(tp));
+	std::tm *tmp = std::localtime(&time_tp);
+	if(!tmp)
+	{
+		return mpt::Date::Local{};
+	}
+	std::tm local = *tmp;
+	mpt::Date::Local result{};
+	result.year = local.tm_year + 1900;
+	result.month = local.tm_mon + 1;
+	result.day = local.tm_mday;
+	result.hours = local.tm_hour;
+	result.minutes = local.tm_min;
+	result.seconds = local.tm_sec;
+	return result;
+#endif
+}
+
+#endif // MODPLUG_TRACKER
+
+#endif
+
 template <LogicalTimezone TZ>
 static mpt::ustring ToShortenedISO8601Impl(mpt::Date::Gregorian<TZ> date)
 {
@@ -193,85 +332,13 @@
 	return ToShortenedISO8601Impl(date);
 }
 
-#if defined(MPT_TIME_CTIME)
-
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc)
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date)
 {
-	int32 daynum = ToDaynum(timeUtc.tm_year+1900, timeUtc.tm_mon+1, timeUtc.tm_mday);
-	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970,1,1))*24*60*60 + timeUtc.tm_hour*60*60 + timeUtc.tm_min*60 + timeUtc.tm_sec;
-	return mpt::Date::UnixFromSeconds(seconds);
+	return ToShortenedISO8601Impl(date);
 }
+#endif // MODPLUG_TRACKER
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime)
-{
-	int64 tmp = mpt::Date::UnixAsSeconds(unixtime);
-	int64 seconds = tmp % 60; tmp /= 60;
-	int64 minutes = tmp % 60; tmp /= 60;
-	int64 hours   = tmp % 24; tmp /= 24;
-	int32 year = 0, month = 0, day = 0;
-	FromDaynum(static_cast<int32>(tmp) + ToDaynum(1970,1,1), year, month, day);
-	tm result = {};
-	result.tm_year = year - 1900;
-	result.tm_mon = month - 1;
-	result.tm_mday = day;
-	result.tm_hour = static_cast<int32>(hours);
-	result.tm_min = static_cast<int32>(minutes);
-	result.tm_sec = static_cast<int32>(seconds);
-	return result;
-}
-
-mpt::ustring ToShortenedISO8601(tm date)
-{
-	// We assume date in UTC here.
-	// There are too many differences in supported format specifiers in strftime()
-	// and strftime does not support reduced precision ISO8601 at all.
-	// Just do the formatting ourselves.
-	mpt::ustring result;
-	mpt::ustring tz = U_("Z");
-	if(date.tm_year == 0)
-	{
-		return result;
-	}
-	result += mpt::ufmt::dec0<4>(date.tm_year + 1900);
-	if(date.tm_mon < 0 || date.tm_mon > 11)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mon + 1);
-	if(date.tm_mday < 1 || date.tm_mday > 31)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mday);
-	if(date.tm_hour == 0 && date.tm_min == 0 && date.tm_sec == 0)
-	{
-		return result;
-	}
-	if(date.tm_hour < 0 || date.tm_hour > 23)
-	{
-		return result;
-	}
-	if(date.tm_min < 0 || date.tm_min > 59)
-	{
-		return result;
-	}
-	result += U_("T");
-	if(date.tm_isdst > 0)
-	{
-		tz = U_("+01:00");
-	}
-	result += mpt::ufmt::dec0<2>(date.tm_hour) + U_(":") + mpt::ufmt::dec0<2>(date.tm_min);
-	if(date.tm_sec < 0 || date.tm_sec > 61)
-	{
-		return result + tz;
-	}
-	result += U_(":") + mpt::ufmt::dec0<2>(date.tm_sec);
-	result += tz;
-	return result;
-}
-
-#endif
-
 } // namespace Date
 } // namespace mpt
 
Index: common/mptTime.h
===================================================================
--- common/mptTime.h	(revision 17427)
+++ common/mptTime.h	(working copy)
@@ -18,16 +18,25 @@
 #include <string>
 
 #if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
-#include <time.h>
+#include <ctime>
 #endif
 
-#define MPT_TIME_CTIME
-
-#if defined(MPT_TIME_CTIME)
-#include <time.h>
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+#if MPT_OS_WINDOWS && defined(_WIN32_WINNT)
+#if (_WIN32_WINNT >= 0x0602) // Win8
+#define MPT_FALLBACK_TIMEZONE_WINDOWS_HISTORIC
+#elif (_WIN32_WINNT >= 0x0501) // WinXP
+#define MPT_FALLBACK_TIMEZONE_WINDOWS_CURRENT
+#else
+#define MPT_FALLBACK_TIMEZONE_C
 #endif
+#else
+#define MPT_FALLBACK_TIMEZONE_C
+#endif
+#endif
 
 
+
 OPENMPT_NAMESPACE_BEGIN
 
 
@@ -99,37 +108,38 @@
 
 using AnyGregorian = Gregorian<LogicalTimezone::Unspecified>;
 
-#if defined(MPT_TIME_CTIME)
-inline tm AsTm(AnyGregorian val)
+using UTC = Gregorian<LogicalTimezone::UTC>;
+
+#if defined(MODPLUG_TRACKER)
+using Local = Gregorian<LogicalTimezone::Local>;
+#endif // MODPLUG_TRACKER
+
+template <LogicalTimezone TZ>
+inline Gregorian<TZ> interpret_as_timezone(AnyGregorian gregorian)
 {
-	tm result{};
-	result.tm_year = val.year - 1900;
-	result.tm_mon = val.month - 1;
-	result.tm_mday = val.day;
-	result.tm_hour = val.hours;
-	result.tm_min = val.minutes;
-	result.tm_sec = static_cast<int>(val.seconds);
+	Gregorian<TZ> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-inline AnyGregorian AsGregorian(tm val)
+
+template <LogicalTimezone TZ>
+inline Gregorian<LogicalTimezone::Unspecified> forget_timezone(Gregorian<TZ> gregorian)
 {
-	AnyGregorian result{};
-	result.year = val.tm_year + 1900;
-	result.month = val.tm_mon + 1;
-	result.day = val.tm_mday;
-	result.hours = val.tm_hour;
-	result.minutes = val.tm_min;
-	result.seconds = val.tm_sec;
+	Gregorian<LogicalTimezone::Unspecified> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-#endif
 
-using UTC = Gregorian<LogicalTimezone::UTC>;
-
-#if defined(MODPLUG_TRACKER)
-using Local = Gregorian<LogicalTimezone::Local>;
-#endif // MODPLUG_TRACKER
-
 #if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 using Unix = std::chrono::system_clock::time_point;
@@ -151,15 +161,15 @@
 
 inline mpt::Date::Unix UnixFromUTC(UTC utc)
 {
-	std::chrono::year_month_day ymd =
-		std::chrono::year{utc.year} /
-		std::chrono::month{utc.month} /
-		std::chrono::day{utc.day};
-	std::chrono::hh_mm_ss<std::chrono::seconds> hms{
-		std::chrono::hours{utc.hours} +
-		std::chrono::minutes{utc.minutes} +
-		std::chrono::seconds{utc.seconds}};
-	return std::chrono::system_clock::time_point{static_cast<std::chrono::sys_days>(ymd)} + hms.to_duration();
+	return std::chrono::system_clock::time_point{
+		std::chrono::sys_days {
+			std::chrono::year{ utc.year } /
+			std::chrono::month{ utc.month } /
+			std::chrono::day{ utc.day }
+		} +
+		std::chrono::hours{ utc.hours } +
+		std::chrono::minutes{ utc.minutes } +
+		std::chrono::seconds{ utc.seconds }};
 }
 
 inline mpt::Date::UTC UnixAsUTC(Unix tp)
@@ -177,6 +187,40 @@
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+inline mpt::Date::Unix UnixFromLocal(Local local)
+{
+	std::chrono::time_point<std::chrono::local_t, std::chrono::seconds> local_tp = 
+		std::chrono::local_days {
+			std::chrono::year{ local.year } /
+			std::chrono::month{ local.month } /
+			std::chrono::day{ local.day }
+		} +
+		std::chrono::hours{ local.hours } +
+		std::chrono::minutes{ local.minutes } +
+		std::chrono::seconds{ local.seconds };
+	return std::chrono::zoned_time{std::chrono::current_zone(), local_tp}.get_sys_time();
+}
+
+inline mpt::Date::Local UnixAsLocal(Unix tp)
+{
+	std::chrono::zoned_time local_tp{ std::chrono::current_zone(), tp };
+	std::chrono::local_days dp = std::chrono::floor<std::chrono::days>(local_tp.get_local_time());
+	std::chrono::year_month_day ymd{dp};
+	std::chrono::hh_mm_ss hms{local_tp.get_local_time() - dp};
+	mpt::Date::Local result;
+	result.year = static_cast<int>(ymd.year());
+	result.month = static_cast<unsigned int>(ymd.month());
+	result.day = static_cast<unsigned int>(ymd.day());
+	result.hours = hms.hours().count();
+	result.minutes = hms.minutes().count();
+	result.seconds = hms.seconds().count();
+	return result;
+}
+
+#endif // MODPLUG_TRACKER
+
 #else
 
 // int64 counts 1s since 1970-01-01T00:00Z
@@ -195,7 +239,7 @@
 
 inline Unix UnixNow()
 {
-	return Unix{static_cast<int64>(time(nullptr))};
+	return Unix{static_cast<int64>(std::time(nullptr))};
 }
 
 inline int64 UnixAsSeconds(Unix tp)
@@ -212,21 +256,23 @@
 
 mpt::Date::UTC UnixAsUTC(Unix tp);
 
-#endif
+#if defined(MODPLUG_TRACKER)
 
-mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
+mpt::Date::Unix UnixFromLocal(Local timeLocal);
 
-mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
+mpt::Date::Local UnixAsLocal(Unix tp);
 
-#if defined(MPT_TIME_CTIME)
+#endif // MODPLUG_TRACKER
 
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc);
+#endif
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime);
+mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
 
-mpt::ustring ToShortenedISO8601(tm date); // i.e. 2015-01-15T18:32:01Z
+mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
 
-#endif
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date); // i.e. 2015-01-15T18:32:01
+#endif // MODPLUG_TRACKER
 
 } // namespace Date
 } // namespace mpt
Index: common/versionNumber.h
===================================================================
--- common/versionNumber.h	(revision 17427)
+++ common/versionNumber.h	(working copy)
@@ -18,6 +18,6 @@
 #define VER_MAJORMAJOR  1
 #define VER_MAJOR      31
 #define VER_MINOR      00
-#define VER_MINORMINOR 12
+#define VER_MINORMINOR 13
 
 OPENMPT_NAMESPACE_END
Index: libopenmpt/libopenmpt_impl.cpp
===================================================================
--- libopenmpt/libopenmpt_impl.cpp	(revision 17427)
+++ libopenmpt/libopenmpt_impl.cpp	(working copy)
@@ -1241,7 +1241,7 @@
 		if ( m_sndFile->GetFileHistory().empty() || !m_sndFile->GetFileHistory().back().HasValidDate() ) {
 			return std::string();
 		}
-		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601() );
+		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601( m_sndFile->GetTimezoneInternal() ) );
 	} else if ( key == std::string("message") ) {
 		std::string retval = m_sndFile->m_songMessage.GetFormatted( OpenMPT::SongMessage::leLF );
 		if ( retval.empty() ) {
Index: mptrack/dlg_misc.cpp
===================================================================
--- mptrack/dlg_misc.cpp	(revision 17427)
+++ mptrack/dlg_misc.cpp	(working copy)
@@ -1311,18 +1311,15 @@
 	for(const auto &entry : editHistory)
 	{
 		totalTime += entry.openTime;
-
 		// Date
-		CString sDate;
+		CString sDate = CString(_T("<unknown date>"));
 		if(entry.HasValidDate())
 		{
-			TCHAR szDate[32];
-			const tm loadDate = mpt::Date::AsTm(entry.loadDate);
-			_tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &loadDate);
-			sDate = szDate;
-		} else
-		{
-			sDate = _T("<unknown date>");
+			const mpt::Date::Unix unixdate = ((m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local) || (m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Unspecified))
+				? mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(entry.loadDate))
+				: mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(entry.loadDate));
+				;
+			sDate = CTime(mpt::Date::UnixAsSeconds(unixdate)).Format(_T("%d %b %Y, %H:%M:%S"));
 		}
 		// Time + stuff
 		uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION);
Index: mptrack/Moddoc.cpp
===================================================================
--- mptrack/Moddoc.cpp	(revision 17427)
+++ mptrack/Moddoc.cpp	(working copy)
@@ -133,7 +133,7 @@
 	, m_InstrumentUndo(*this)
 {
 	// Set the creation date of this file (or the load time if we're loading an existing file)
-	time(&m_creationTime);
+	m_creationTime = mpt::Date::UnixNow();
 
 	ReinitRecordState();
 
Index: mptrack/Moddoc.h
===================================================================
--- mptrack/Moddoc.h	(revision 17427)
+++ mptrack/Moddoc.h	(working copy)
@@ -14,10 +14,10 @@
 
 #include "Sndfile.h"
 #include "../common/misc_util.h"
+#include "../common/mptTime.h"
 #include "Undo.h"
 #include "Notification.h"
 #include "UpdateHints.h"
-#include <time.h>
 
 OPENMPT_NAMESPACE_BEGIN
 
@@ -130,7 +130,7 @@
 	CSampleUndo m_SampleUndo;
 	CInstrumentUndo m_InstrumentUndo;
 	SplitKeyboardSettings m_SplitKeyboardSettings;	// this is maybe not the best place to keep them, but it should do the job
-	time_t m_creationTime;
+	mpt::Date::Unix m_creationTime;
 
 	std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave?
 
@@ -221,7 +221,7 @@
 	CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; }
 	SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; }
 
-	time_t GetCreationTime() const { return m_creationTime; }
+	mpt::Date::Unix GetCreationTime() const { return m_creationTime; }
 
 // operations
 public:
Index: mptrack/Mptrack.cpp
===================================================================
--- mptrack/Mptrack.cpp	(revision 17427)
+++ mptrack/Mptrack.cpp	(working copy)
@@ -631,6 +631,24 @@
 }
 
 
+CTrackApp::~CTrackApp()
+{
+#if !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) && defined(MPT_LIBCXX_QUIRK_CHRONO_TZ_MEMLEAK)
+	// Work-around memleak (see <https://github.com/microsoft/STL/issues/2504#issuecomment-1068008937>)
+	try
+	{
+		std::chrono::get_tzdb_list().~tzdb_list();
+	} catch(const std::exception &)
+	{
+		// nothing
+	} catch(...)
+	{
+		// nothing
+	}
+#endif
+}
+
+
 class OpenMPTDataRecoveryHandler
 	: public CDataRecoveryHandler
 {
Index: mptrack/Mptrack.h
===================================================================
--- mptrack/Mptrack.h	(revision 17427)
+++ mptrack/Mptrack.h	(working copy)
@@ -166,6 +166,7 @@
 
 public:
 	CTrackApp();
+	~CTrackApp();
 
 	CDataRecoveryHandler *GetDataRecoveryHandler() override;
 	void AddToRecentFileList(LPCTSTR lpszPathName) override;
Index: mptrack/OPLExport.cpp
===================================================================
--- mptrack/OPLExport.cpp	(revision 17427)
+++ mptrack/OPLExport.cpp	(working copy)
@@ -397,7 +397,7 @@
 		SetDlgItemText(IDC_EDIT2, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.GetTitle()).c_str());
 		SetDlgItemText(IDC_EDIT3, mpt::ToWin(m_sndFile.m_songArtist).c_str());
 		if(!m_sndFile.GetFileHistory().empty())
-			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601().substr(0, 10), U_("-"), U_("/"))).c_str());
+			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601(m_sndFile.GetTimezoneInternal()).substr(0, 10), U_("-"), U_("/"))).c_str());
 		SetDlgItemText(IDC_EDIT5, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.m_songMessage.GetFormatted(SongMessage::leCRLF)).c_str());
 
 		m_locked = false;
Index: soundlib/Load_it.cpp
===================================================================
--- soundlib/Load_it.cpp	(revision 17427)
+++ soundlib/Load_it.cpp	(working copy)
@@ -1278,6 +1278,11 @@
 	m_modFormat.type = (GetType() == MOD_TYPE_MPT) ? U_("mptm") : U_("it");
 	m_modFormat.madeWithTracker = std::move(madeWithTracker);
 	m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::Charset::Windows1252 : mpt::Charset::CP437;
+#ifdef MODPLUG_TRACKER
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_V("1.31.00.13"))) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Local;
+#else
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_V("1.31.00.13"))) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Unspecified;
+#endif
 
 	return true;
 }
@@ -1359,15 +1364,19 @@
 		} else if(pModDoc != nullptr)
 		{
 			// Current ("new") timestamp
-			const time_t creationTime = pModDoc->GetCreationTime();
-			mptHistory.loadDate = mpt::Date::AnyGregorian{};
-			//localtime_s(&loadDate, &creationTime);
-			const tm* const p = localtime(&creationTime);
-			if (p != nullptr)
-				mptHistory.loadDate = mpt::Date::AsGregorian(*p);
-			else
-				sndFile.AddToLog(LogError, U_("Unable to retrieve current time."));
-			mptHistory.openTime = (uint32)(difftime(time(nullptr), creationTime) * HISTORY_TIMER_PRECISION);
+			const mpt::Date::Unix creationTime = pModDoc->GetCreationTime();
+			if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			} else if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(creationTime));
+			} else
+			{
+				// assume UTC
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			}
+			mptHistory.openTime = static_cast<uint32>(mpt::round((mpt::Date::UnixAsSeconds(mpt::Date::UnixNow()) - mpt::Date::UnixAsSeconds(creationTime)) * HISTORY_TIMER_PRECISION));
 #endif // MODPLUG_TRACKER
 		}
 
Index: soundlib/Load_mod.cpp
===================================================================
--- soundlib/Load_mod.cpp	(revision 17427)
+++ soundlib/Load_mod.cpp	(working copy)
@@ -2179,6 +2179,11 @@
 		if(mpt::is_in_range(info.dateMonth, 1, 12) && mpt::is_in_range(info.dateDay, 1, 31) && mpt::is_in_range(info.dateHour, 0, 23)
 		   && mpt::is_in_range(info.dateMinute, 0, 59) && mpt::is_in_range(info.dateSecond, 0, 59))
 		{
+#ifdef MODPLUG_TRACKER
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Local;
+#else
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Unspecified;
+#endif
 			FileHistory mptHistory;
 			mptHistory.loadDate.year = info.dateYear + 1900;
 			mptHistory.loadDate.month = info.dateMonth;
Index: soundlib/Sndfile.cpp
===================================================================
--- soundlib/Sndfile.cpp	(revision 17427)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -54,19 +54,47 @@
 }
 
 
-mpt::ustring FileHistory::AsISO8601() const
+mpt::ustring FileHistory::AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const
 {
-	tm date = mpt::Date::AsTm(loadDate);
 	if(openTime > 0)
 	{
 		// Calculate the date when editing finished.
 		double openSeconds = static_cast<double>(openTime) / HISTORY_TIMER_PRECISION;
-		tm tmpLoadDate = mpt::Date::AsTm(loadDate);
-		int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTCtm(tmpLoadDate));
-		int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
-		date = mpt::Date::UnixAsUTCtm(mpt::Date::UnixFromSeconds(saveDateSinceEpoch));
+		mpt::Date::AnyGregorian tmpLoadDate = loadDate;
+		if (internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsLocal(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			// assume UTC for unspecified timezone when calculating
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch))));
+		}
+	} else
+	{
+		if(internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(loadDate));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(loadDate));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			return mpt::Date::ToShortenedISO8601(loadDate);
+		}
 	}
-	return mpt::Date::ToShortenedISO8601(date);
 }
 
 
@@ -483,8 +511,24 @@
 		InitializeGlobals();
 		m_visitedRows.Initialize(true);
 		m_dwCreatedWithVersion = Version::Current();
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
 	}
 
+#ifdef MODPLUG_TRACKER
+	// convert timestamps to UTC
+	if(m_modFormat.timezone == mpt::Date::LogicalTimezone::Local)
+	{
+		for(auto & fileHistoryEntry : m_FileHistory)
+		{
+			if(fileHistoryEntry.HasValidDate())
+			{
+				fileHistoryEntry.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(fileHistoryEntry.loadDate))));
+			}
+		}
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
+	}
+#endif // MODPLUG_TRACKER
+
 	// Adjust channels
 	const auto muteFlag = GetChannelMuteFlag();
 	for(CHANNELINDEX chn = 0; chn < MAX_BASECHANNELS; chn++)
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 17427)
+++ soundlib/Sndfile.h	(working copy)
@@ -247,7 +247,7 @@
 	// Time the file was open in the editor, in 1/18.2th seconds (frequency of a standard DOS timer, to keep compatibility with Impulse Tracker easy).
 	uint32 openTime = 0;
 	// Return the date as a (possibly truncated if not enough precision is available) ISO 8601 formatted date.
-	mpt::ustring AsISO8601() const;
+	mpt::ustring AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const;
 	// Returns true if the date component is valid. Some formats only store edit time, not edit date.
 	bool HasValidDate() const
 	{
@@ -285,6 +285,7 @@
 	mpt::ustring originalFormatName; // "FastTracker 2" in the case of converted formats like MO3 or GDM
 	mpt::ustring originalType;       // "xm" in the case of converted formats like MO3 or GDM
 	mpt::Charset charset = mpt::Charset::UTF8;
+	mpt::Date::LogicalTimezone timezone = mpt::Date::LogicalTimezone::Unspecified;
 };
 
 
@@ -752,6 +753,10 @@
 			return GetCharsetFile();
 		#endif // MODPLUG_TRACKER
 	}
+	mpt::Date::LogicalTimezone GetTimezoneInternal() const
+	{
+		return m_modFormat.timezone;
+	}
 
 	ModMessageHeuristicOrder GetMessageHeuristic() const;
 
Index: src/mpt/base/detect_quirks.hpp
===================================================================
--- src/mpt/base/detect_quirks.hpp	(revision 17427)
+++ src/mpt/base/detect_quirks.hpp	(working copy)
@@ -235,13 +235,15 @@
 #define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
 #endif
 #if MPT_LIBCXX_MS && (MPT_MSVC_BEFORE(2022, 2) || !MPT_COMPILER_MSVC)
+#elif MPT_LIBCXX_GNU
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#endif
+#if MPT_LIBCXX_MS && (MPT_MSVC_BEFORE(2022, 3) || !MPT_COMPILER_MSVC)
 // Causes massive memory leaks.
 // See
 // <https://developercommunity.visualstudio.com/t/stdchronoget-tzdb-list-memory-leak/1644641>
 // / <https://github.com/microsoft/STL/issues/2504>.
-#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
-#elif MPT_LIBCXX_GNU
-#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#define MPT_LIBCXX_QUIRK_CHRONO_TZ_MEMLEAK
 #endif
 #endif
 
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 17427)
+++ test/test.cpp	(working copy)
@@ -2096,12 +2096,37 @@
 	// Edit history
 	VERIFY_EQUAL_NONCONT(sndFile.GetFileHistory().size() > 15, true);
 	const FileHistory &fh = sndFile.GetFileHistory().front();
-	VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
+#ifdef MODPLUG_TRACKER
+	if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+	{
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).year, 2011);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).month, 6);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).day, 14);
+#if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 21);
+#else
+#if defined(MPT_FALLBACK_TIMEZONE_WINDOWS_HISTORIC)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 21);
+#elif defined(MPT_FALLBACK_TIMEZONE_WINDOWS_CURRENT)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 21);
+#elif defined(MPT_FALLBACK_TIMEZONE_C)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 22);
+#else
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 22);
+#endif
+#endif
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).minutes, 8);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).seconds, 32);
+	} else
+#endif // MODPLUG_TRACKER
+	{
+		VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
+	}
 	VERIFY_EQUAL_NONCONT((uint32)((double)fh.openTime / HISTORY_TIMER_PRECISION), 31);
 
 	// Macros
chrono-v15.patch (32,627 bytes)   
manx

manx

2022-07-09 11:56

administrator   ~0005250

[Mod] Edit History: Dates in Edit History can now have 1 of 3 timezone designators: UTC, Local, Unspecified.
[Mod] Edit History: When the timezone is Unspecified, dates are now formatted without the UTC timezone signifier 'Z' in ISO8601 format. This affects libopenmpt.
[Mod] IT/MPTM: Dates can now be written in UTC. Loading can decide by looking at m_dwLastSavedWithVersion how to interpret dates. After loading, all dates with Local timezone can be converted into UTC timezone, dates with Unspecified timezone are kept as is. This is guarded by a currently disabled MPT_TIME_UTC_ON_DISK feature flag.
[Mod] MOD: Assume dates are in Local timezone.
[Ref] Edit History: OpenMPT still displays dates in Local timezone, and assumes so also for Unspecified timezone dates.
[Imp] When calculating save time from load time and open duration, properly take Local timezone into account. This uses C++20 tzdb (for C++20 or later), or Windows historic timezone information (for Windows 8 or later), or current Windows timezone information (for Windows XP or later), or C stdlib implementation (otherwise).
[Reg/Fix] On pre-C++20 pre-WinXP builds, Local time stored during DST will now be interpreted as non-DST. This is a fundamental limitation of the C time.h API that does not allow to determine whether a specific Local time has DST active or not from the timestamp alone (or whether it is ambiguous). As no module format stores the isdst flag of struct tm, this information has always been always lost. Timestamp + duration calculation have always been wrong here when crossing DST change. The alternative to this regression/misinterpretation would be to always do it wrong forever.
[Fix] Work-around MSVC tzdb memleak. See &lt;https://github.com/microsoft/STL/issues/2504#issuecomment-1068008937>.
[Ref] mptTime: Remove all &lt;ctime> usage for modern builds.
chrono-v16.patch (33,551 bytes)   
Index: build/pch/PCH.h
===================================================================
--- build/pch/PCH.h	(revision 17650)
+++ build/pch/PCH.h	(working copy)
@@ -153,7 +153,6 @@
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
-#include <time.h>
 
 
 #endif // MPT_BUILD_ENABLE_PCH
Index: common/BuildSettings.h
===================================================================
--- common/BuildSettings.h	(revision 17650)
+++ common/BuildSettings.h	(working copy)
@@ -321,8 +321,11 @@
 
 #endif
 
+#define MPT_TIME_UTC_ON_DISK 0
+#define MPT_TIME_UTC_ON_DISK_VERSION MPT_V("1.31.00.13")
 
 
+
 // fixing stuff up
 
 #if defined(MPT_BUILD_ANALYZED) || defined(MPT_BUILD_CHECKED) 
Index: common/mptTime.cpp
===================================================================
--- common/mptTime.cpp	(revision 17650)
+++ common/mptTime.cpp	(working copy)
@@ -83,7 +83,7 @@
 
 #endif // MODPLUG_TRACKER
 
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) || defined(MPT_TIME_CTIME)
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 static int32 ToDaynum(int32 year, int32 month, int32 day)
 {
@@ -115,10 +115,6 @@
 	day = static_cast<int32>(dd);
 }
 
-#endif
-
-#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
-
 mpt::Date::Unix UnixFromUTC(UTC timeUtc)
 {
 	int32 daynum = ToDaynum(timeUtc.year, timeUtc.month, timeUtc.day);
@@ -140,12 +136,155 @@
 	result.day = day;
 	result.hours = static_cast<int32>(hours);
 	result.minutes = static_cast<int32>(minutes);
-	result.seconds = static_cast<int64>(seconds);
+	result.seconds = seconds;
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+mpt::Date::Unix UnixFromLocal(Local timeLocal)
+{
+#if defined(MPT_FALLBACK_TIMEZONE_WINDOWS_HISTORIC)
+	SYSTEMTIME sys_local{};
+	sys_local.wYear = static_cast<uint16>(timeLocal.year);
+	sys_local.wMonth = static_cast<uint16>(timeLocal.month);
+	sys_local.wDay = static_cast<uint16>(timeLocal.day);
+	sys_local.wHour = static_cast<uint16>(timeLocal.hours);
+	sys_local.wMinute = static_cast<uint16>(timeLocal.minutes);
+	sys_local.wSecond = static_cast<uint16>(timeLocal.seconds);
+	sys_local.wMilliseconds = 0;
+	DYNAMIC_TIME_ZONE_INFORMATION dtzi{};
+	if(GetDynamicTimeZoneInformation(&dtzi) == TIME_ZONE_ID_INVALID) // WinVista
+	{
+		return mpt::Date::Unix{};
+	}
+	SYSTEMTIME sys_utc{};
+	if(TzSpecificLocalTimeToSystemTimeEx(&dzti, &sys_local, &sys_utc) == FALSE) // Win7/Win8
+	{
+		return mpt::Date::Unix{};
+	}
+	FILETIME ft{};
+	if(SystemTimeToFileTime(&sys_utc, &ft) == FALSE) // Win 2000
+	{
+		return mpt::Date::Unix{};
+	}
+	ULARGE_INTEGER time_value{};
+	time_value.LowPart = ft.dwLowDateTime;
+	time_value.HighPart = ft.dwHighDateTime;
+	return mpt::Date::UnixFromSeconds(static_cast<int64>((time_value.QuadPart - 116444736000000000LL) / 10000000LL));
+#elif defined(MPT_FALLBACK_TIMEZONE_WINDOWS_CURRENT)
+	SYSTEMTIME sys_local{};
+	sys_local.wYear = static_cast<uint16>(timeLocal.year);
+	sys_local.wMonth = static_cast<uint16>(timeLocal.month);
+	sys_local.wDay = static_cast<uint16>(timeLocal.day);
+	sys_local.wHour = static_cast<uint16>(timeLocal.hours);
+	sys_local.wMinute = static_cast<uint16>(timeLocal.minutes);
+	sys_local.wSecond = static_cast<uint16>(timeLocal.seconds);
+	sys_local.wMilliseconds = 0;
+	SYSTEMTIME sys_utc{};
+	if(TzSpecificLocalTimeToSystemTime(NULL, &sys_local, &sys_utc) == FALSE) // WinXP
+	{
+		return mpt::Date::Unix{};
+	}
+	FILETIME ft{};
+	if(SystemTimeToFileTime(&sys_utc, &ft) == FALSE) // Win 2000
+	{
+		return mpt::Date::Unix{};
+	}
+	ULARGE_INTEGER time_value{};
+	time_value.LowPart = ft.dwLowDateTime;
+	time_value.HighPart = ft.dwHighDateTime;
+	return mpt::Date::UnixFromSeconds(static_cast<int64>((time_value.QuadPart - 116444736000000000LL) / 10000000LL));
+#elif defined(MPT_FALLBACK_TIMEZONE_C)
+	std::tm tmp{};
+	tmp.tm_year = timeLocal.year - 1900;
+	tmp.tm_mon = timeLocal.month - 1;
+	tmp.tm_mday = timeLocal.day;
+	tmp.tm_hour = timeLocal.hours;
+	tmp.tm_min = timeLocal.minutes;
+	tmp.tm_sec = static_cast<int>(timeLocal.seconds);
+	return mpt::Date::UnixFromSeconds(static_cast<int64>(std::mktime(&tmp)));
 #endif
+}
 
+mpt::Date::Local UnixAsLocal(Unix tp)
+{
+#if defined(MPT_FALLBACK_TIMEZONE_WINDOWS_HISTORIC)
+	ULARGE_INTEGER time_value{};
+	time_value.QuadPart = static_cast<int64>(mpt::Date::UnixAsSeconds(tp)) * 10000000LL + 116444736000000000LL;
+	FILETIME ft{};
+	ft.dwLowDateTime = time_value.LowPart;
+	ft.dwHighDateTime = time_value.HighPart;
+	SYSTEMTIME sys_utc{};
+	if(FileTimeToSystemTime(&ft, &sys_utc) == FALSE) // WinXP
+	{
+		return mpt::Date::Local{};
+	}
+	DYNAMIC_TIME_ZONE_INFORMATION dtzi{};
+	if(GetDynamicTimeZoneInformation(&dtzi) == TIME_ZONE_ID_INVALID) // WinVista
+	{
+		return mpt::Date::Local{};
+	}
+	SYSTEMTIME sys_local{};
+	if(SystemTimeToTzSpecificLocalTimeEx(&dzti, &sys_utc, &sys_local) == FALSE) // Win7/Win8
+	{
+		return mpt::Date::Local{};
+	}
+	mpt::Date::Local result{};
+	result.year = sys_local.wYear;
+	result.month = sys_local.wMonth;
+	result.day = sys_local.wDay;
+	result.hours = sys_local.wHour;
+	result.minutes = sys_local.wMinute;
+	result.seconds = sys_local.wSecond;
+	return result;
+#elif defined(MPT_FALLBACK_TIMEZONE_WINDOWS_CURRENT)
+	ULARGE_INTEGER time_value{};
+	time_value.QuadPart = static_cast<int64>(mpt::Date::UnixAsSeconds(tp)) * 10000000LL + 116444736000000000LL;
+	FILETIME ft{};
+	ft.dwLowDateTime = time_value.LowPart;
+	ft.dwHighDateTime = time_value.HighPart;
+	SYSTEMTIME sys_utc{};
+	if(FileTimeToSystemTime(&ft, &sys_utc) == FALSE) // WinXP
+	{
+		return mpt::Date::Local{};
+	}
+	SYSTEMTIME sys_local{};
+	if(SystemTimeToTzSpecificLocalTime(NULL, &sys_utc, &sys_local) == FALSE) // Win2000
+	{
+		return mpt::Date::Local{};
+	}
+	mpt::Date::Local result{};
+	result.year = sys_local.wYear;
+	result.month = sys_local.wMonth;
+	result.day = sys_local.wDay;
+	result.hours = sys_local.wHour;
+	result.minutes = sys_local.wMinute;
+	result.seconds = sys_local.wSecond;
+	return result;
+#elif defined(MPT_FALLBACK_TIMEZONE_C)
+	std::time_t time_tp = static_cast<std::time_t>(mpt::Date::UnixAsSeconds(tp));
+	std::tm *tmp = std::localtime(&time_tp);
+	if(!tmp)
+	{
+		return mpt::Date::Local{};
+	}
+	std::tm local = *tmp;
+	mpt::Date::Local result{};
+	result.year = local.tm_year + 1900;
+	result.month = local.tm_mon + 1;
+	result.day = local.tm_mday;
+	result.hours = local.tm_hour;
+	result.minutes = local.tm_min;
+	result.seconds = local.tm_sec;
+	return result;
+#endif
+}
+
+#endif // MODPLUG_TRACKER
+
+#endif
+
 template <LogicalTimezone TZ>
 static mpt::ustring ToShortenedISO8601Impl(mpt::Date::Gregorian<TZ> date)
 {
@@ -193,85 +332,13 @@
 	return ToShortenedISO8601Impl(date);
 }
 
-#if defined(MPT_TIME_CTIME)
-
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc)
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date)
 {
-	int32 daynum = ToDaynum(timeUtc.tm_year+1900, timeUtc.tm_mon+1, timeUtc.tm_mday);
-	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970,1,1))*24*60*60 + timeUtc.tm_hour*60*60 + timeUtc.tm_min*60 + timeUtc.tm_sec;
-	return mpt::Date::UnixFromSeconds(seconds);
+	return ToShortenedISO8601Impl(date);
 }
+#endif // MODPLUG_TRACKER
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime)
-{
-	int64 tmp = mpt::Date::UnixAsSeconds(unixtime);
-	int64 seconds = tmp % 60; tmp /= 60;
-	int64 minutes = tmp % 60; tmp /= 60;
-	int64 hours   = tmp % 24; tmp /= 24;
-	int32 year = 0, month = 0, day = 0;
-	FromDaynum(static_cast<int32>(tmp) + ToDaynum(1970,1,1), year, month, day);
-	tm result = {};
-	result.tm_year = year - 1900;
-	result.tm_mon = month - 1;
-	result.tm_mday = day;
-	result.tm_hour = static_cast<int32>(hours);
-	result.tm_min = static_cast<int32>(minutes);
-	result.tm_sec = static_cast<int32>(seconds);
-	return result;
-}
-
-mpt::ustring ToShortenedISO8601(tm date)
-{
-	// We assume date in UTC here.
-	// There are too many differences in supported format specifiers in strftime()
-	// and strftime does not support reduced precision ISO8601 at all.
-	// Just do the formatting ourselves.
-	mpt::ustring result;
-	mpt::ustring tz = U_("Z");
-	if(date.tm_year == 0)
-	{
-		return result;
-	}
-	result += mpt::ufmt::dec0<4>(date.tm_year + 1900);
-	if(date.tm_mon < 0 || date.tm_mon > 11)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mon + 1);
-	if(date.tm_mday < 1 || date.tm_mday > 31)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mday);
-	if(date.tm_hour == 0 && date.tm_min == 0 && date.tm_sec == 0)
-	{
-		return result;
-	}
-	if(date.tm_hour < 0 || date.tm_hour > 23)
-	{
-		return result;
-	}
-	if(date.tm_min < 0 || date.tm_min > 59)
-	{
-		return result;
-	}
-	result += U_("T");
-	if(date.tm_isdst > 0)
-	{
-		tz = U_("+01:00");
-	}
-	result += mpt::ufmt::dec0<2>(date.tm_hour) + U_(":") + mpt::ufmt::dec0<2>(date.tm_min);
-	if(date.tm_sec < 0 || date.tm_sec > 61)
-	{
-		return result + tz;
-	}
-	result += U_(":") + mpt::ufmt::dec0<2>(date.tm_sec);
-	result += tz;
-	return result;
-}
-
-#endif
-
 } // namespace Date
 } // namespace mpt
 
Index: common/mptTime.h
===================================================================
--- common/mptTime.h	(revision 17650)
+++ common/mptTime.h	(working copy)
@@ -18,16 +18,25 @@
 #include <string>
 
 #if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
-#include <time.h>
+#include <ctime>
 #endif
 
-#define MPT_TIME_CTIME
-
-#if defined(MPT_TIME_CTIME)
-#include <time.h>
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+#if MPT_OS_WINDOWS && defined(_WIN32_WINNT)
+#if (_WIN32_WINNT >= 0x0602) // Win8
+#define MPT_FALLBACK_TIMEZONE_WINDOWS_HISTORIC
+#elif (_WIN32_WINNT >= 0x0501) // WinXP
+#define MPT_FALLBACK_TIMEZONE_WINDOWS_CURRENT
+#else
+#define MPT_FALLBACK_TIMEZONE_C
 #endif
+#else
+#define MPT_FALLBACK_TIMEZONE_C
+#endif
+#endif
 
 
+
 OPENMPT_NAMESPACE_BEGIN
 
 
@@ -99,37 +108,38 @@
 
 using AnyGregorian = Gregorian<LogicalTimezone::Unspecified>;
 
-#if defined(MPT_TIME_CTIME)
-inline tm AsTm(AnyGregorian val)
+using UTC = Gregorian<LogicalTimezone::UTC>;
+
+#if defined(MODPLUG_TRACKER)
+using Local = Gregorian<LogicalTimezone::Local>;
+#endif // MODPLUG_TRACKER
+
+template <LogicalTimezone TZ>
+inline Gregorian<TZ> interpret_as_timezone(AnyGregorian gregorian)
 {
-	tm result{};
-	result.tm_year = val.year - 1900;
-	result.tm_mon = val.month - 1;
-	result.tm_mday = val.day;
-	result.tm_hour = val.hours;
-	result.tm_min = val.minutes;
-	result.tm_sec = static_cast<int>(val.seconds);
+	Gregorian<TZ> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-inline AnyGregorian AsGregorian(tm val)
+
+template <LogicalTimezone TZ>
+inline Gregorian<LogicalTimezone::Unspecified> forget_timezone(Gregorian<TZ> gregorian)
 {
-	AnyGregorian result{};
-	result.year = val.tm_year + 1900;
-	result.month = val.tm_mon + 1;
-	result.day = val.tm_mday;
-	result.hours = val.tm_hour;
-	result.minutes = val.tm_min;
-	result.seconds = val.tm_sec;
+	Gregorian<LogicalTimezone::Unspecified> result;
+	result.year = gregorian.year;
+	result.month = gregorian.month;
+	result.day = gregorian.day;
+	result.hours = gregorian.hours;
+	result.minutes = gregorian.minutes;
+	result.seconds = gregorian.seconds;
 	return result;
 }
-#endif
 
-using UTC = Gregorian<LogicalTimezone::UTC>;
-
-#if defined(MODPLUG_TRACKER)
-using Local = Gregorian<LogicalTimezone::Local>;
-#endif // MODPLUG_TRACKER
-
 #if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
 using Unix = std::chrono::system_clock::time_point;
@@ -151,15 +161,15 @@
 
 inline mpt::Date::Unix UnixFromUTC(UTC utc)
 {
-	std::chrono::year_month_day ymd =
-		std::chrono::year{utc.year} /
-		std::chrono::month{utc.month} /
-		std::chrono::day{utc.day};
-	std::chrono::hh_mm_ss<std::chrono::seconds> hms{
-		std::chrono::hours{utc.hours} +
-		std::chrono::minutes{utc.minutes} +
-		std::chrono::seconds{utc.seconds}};
-	return std::chrono::system_clock::time_point{static_cast<std::chrono::sys_days>(ymd)} + hms.to_duration();
+	return std::chrono::system_clock::time_point{
+		std::chrono::sys_days {
+			std::chrono::year{ utc.year } /
+			std::chrono::month{ utc.month } /
+			std::chrono::day{ utc.day }
+		} +
+		std::chrono::hours{ utc.hours } +
+		std::chrono::minutes{ utc.minutes } +
+		std::chrono::seconds{ utc.seconds }};
 }
 
 inline mpt::Date::UTC UnixAsUTC(Unix tp)
@@ -177,6 +187,40 @@
 	return result;
 }
 
+#if defined(MODPLUG_TRACKER)
+
+inline mpt::Date::Unix UnixFromLocal(Local local)
+{
+	std::chrono::time_point<std::chrono::local_t, std::chrono::seconds> local_tp = 
+		std::chrono::local_days {
+			std::chrono::year{ local.year } /
+			std::chrono::month{ local.month } /
+			std::chrono::day{ local.day }
+		} +
+		std::chrono::hours{ local.hours } +
+		std::chrono::minutes{ local.minutes } +
+		std::chrono::seconds{ local.seconds };
+	return std::chrono::zoned_time{std::chrono::current_zone(), local_tp}.get_sys_time();
+}
+
+inline mpt::Date::Local UnixAsLocal(Unix tp)
+{
+	std::chrono::zoned_time local_tp{ std::chrono::current_zone(), tp };
+	std::chrono::local_days dp = std::chrono::floor<std::chrono::days>(local_tp.get_local_time());
+	std::chrono::year_month_day ymd{dp};
+	std::chrono::hh_mm_ss hms{local_tp.get_local_time() - dp};
+	mpt::Date::Local result;
+	result.year = static_cast<int>(ymd.year());
+	result.month = static_cast<unsigned int>(ymd.month());
+	result.day = static_cast<unsigned int>(ymd.day());
+	result.hours = hms.hours().count();
+	result.minutes = hms.minutes().count();
+	result.seconds = hms.seconds().count();
+	return result;
+}
+
+#endif // MODPLUG_TRACKER
+
 #else
 
 // int64 counts 1s since 1970-01-01T00:00Z
@@ -195,7 +239,7 @@
 
 inline Unix UnixNow()
 {
-	return Unix{static_cast<int64>(time(nullptr))};
+	return Unix{static_cast<int64>(std::time(nullptr))};
 }
 
 inline int64 UnixAsSeconds(Unix tp)
@@ -212,21 +256,23 @@
 
 mpt::Date::UTC UnixAsUTC(Unix tp);
 
-#endif
+#if defined(MODPLUG_TRACKER)
 
-mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
+mpt::Date::Unix UnixFromLocal(Local timeLocal);
 
-mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
+mpt::Date::Local UnixAsLocal(Unix tp);
 
-#if defined(MPT_TIME_CTIME)
+#endif // MODPLUG_TRACKER
 
-mpt::Date::Unix UnixFromUTCtm(tm timeUtc);
+#endif
 
-tm UnixAsUTCtm(mpt::Date::Unix unixtime);
+mpt::ustring ToShortenedISO8601(AnyGregorian date); // i.e. 2015-01-15T18:32:01
 
-mpt::ustring ToShortenedISO8601(tm date); // i.e. 2015-01-15T18:32:01Z
+mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
 
-#endif
+#ifdef MODPLUG_TRACKER
+mpt::ustring ToShortenedISO8601(Local date); // i.e. 2015-01-15T18:32:01
+#endif // MODPLUG_TRACKER
 
 } // namespace Date
 } // namespace mpt
Index: libopenmpt/libopenmpt_impl.cpp
===================================================================
--- libopenmpt/libopenmpt_impl.cpp	(revision 17650)
+++ libopenmpt/libopenmpt_impl.cpp	(working copy)
@@ -1229,7 +1229,7 @@
 		if ( m_sndFile->GetFileHistory().empty() || !m_sndFile->GetFileHistory().back().HasValidDate() ) {
 			return std::string();
 		}
-		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601() );
+		return mpt::transcode<std::string>( mpt::common_encoding::utf8, m_sndFile->GetFileHistory().back().AsISO8601( m_sndFile->GetTimezoneInternal() ) );
 	} else if ( key == std::string("message") ) {
 		std::string retval = m_sndFile->m_songMessage.GetFormatted( OpenMPT::SongMessage::leLF );
 		if ( retval.empty() ) {
Index: mptrack/dlg_misc.cpp
===================================================================
--- mptrack/dlg_misc.cpp	(revision 17650)
+++ mptrack/dlg_misc.cpp	(working copy)
@@ -1311,18 +1311,15 @@
 	for(const auto &entry : editHistory)
 	{
 		totalTime += entry.openTime;
-
 		// Date
-		CString sDate;
+		CString sDate = CString(_T("<unknown date>"));
 		if(entry.HasValidDate())
 		{
-			TCHAR szDate[32];
-			const tm loadDate = mpt::Date::AsTm(entry.loadDate);
-			_tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &loadDate);
-			sDate = szDate;
-		} else
-		{
-			sDate = _T("<unknown date>");
+			const mpt::Date::Unix unixdate = ((m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local) || (m_modDoc.GetSoundFile().GetTimezoneInternal() == mpt::Date::LogicalTimezone::Unspecified))
+				? mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(entry.loadDate))
+				: mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(entry.loadDate));
+				;
+			sDate = CTime(mpt::Date::UnixAsSeconds(unixdate)).Format(_T("%d %b %Y, %H:%M:%S"));
 		}
 		// Time + stuff
 		uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION);
Index: mptrack/Moddoc.cpp
===================================================================
--- mptrack/Moddoc.cpp	(revision 17650)
+++ mptrack/Moddoc.cpp	(working copy)
@@ -138,7 +138,7 @@
 	, m_InstrumentUndo(*this)
 {
 	// Set the creation date of this file (or the load time if we're loading an existing file)
-	time(&m_creationTime);
+	m_creationTime = mpt::Date::UnixNow();
 
 	ReinitRecordState();
 
Index: mptrack/Moddoc.h
===================================================================
--- mptrack/Moddoc.h	(revision 17650)
+++ mptrack/Moddoc.h	(working copy)
@@ -14,10 +14,10 @@
 
 #include "Sndfile.h"
 #include "../common/misc_util.h"
+#include "../common/mptTime.h"
 #include "Undo.h"
 #include "Notification.h"
 #include "UpdateHints.h"
-#include <time.h>
 
 OPENMPT_NAMESPACE_BEGIN
 
@@ -130,7 +130,7 @@
 	CSampleUndo m_SampleUndo;
 	CInstrumentUndo m_InstrumentUndo;
 	SplitKeyboardSettings m_SplitKeyboardSettings;	// this is maybe not the best place to keep them, but it should do the job
-	time_t m_creationTime;
+	mpt::Date::Unix m_creationTime;
 
 	std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave?
 
@@ -221,7 +221,7 @@
 	CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; }
 	SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; }
 
-	time_t GetCreationTime() const { return m_creationTime; }
+	mpt::Date::Unix GetCreationTime() const { return m_creationTime; }
 
 // operations
 public:
Index: mptrack/Mptrack.cpp
===================================================================
--- mptrack/Mptrack.cpp	(revision 17650)
+++ mptrack/Mptrack.cpp	(working copy)
@@ -634,6 +634,24 @@
 }
 
 
+CTrackApp::~CTrackApp()
+{
+#if !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE) && defined(MPT_LIBCXX_QUIRK_CHRONO_TZ_MEMLEAK)
+	// Work-around memleak (see <https://github.com/microsoft/STL/issues/2504#issuecomment-1068008937>)
+	try
+	{
+		std::chrono::get_tzdb_list().~tzdb_list();
+	} catch(const std::exception &)
+	{
+		// nothing
+	} catch(...)
+	{
+		// nothing
+	}
+#endif
+}
+
+
 class OpenMPTDataRecoveryHandler
 	: public CDataRecoveryHandler
 {
Index: mptrack/Mptrack.h
===================================================================
--- mptrack/Mptrack.h	(revision 17650)
+++ mptrack/Mptrack.h	(working copy)
@@ -166,6 +166,7 @@
 
 public:
 	CTrackApp();
+	~CTrackApp();
 
 	CDataRecoveryHandler *GetDataRecoveryHandler() override;
 	void AddToRecentFileList(LPCTSTR lpszPathName) override;
Index: mptrack/OPLExport.cpp
===================================================================
--- mptrack/OPLExport.cpp	(revision 17650)
+++ mptrack/OPLExport.cpp	(working copy)
@@ -398,7 +398,7 @@
 		SetDlgItemText(IDC_EDIT2, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.GetTitle()).c_str());
 		SetDlgItemText(IDC_EDIT3, mpt::ToWin(m_sndFile.m_songArtist).c_str());
 		if(!m_sndFile.GetFileHistory().empty())
-			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601().substr(0, 10), U_("-"), U_("/"))).c_str());
+			SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601(m_sndFile.GetTimezoneInternal()).substr(0, 10), U_("-"), U_("/"))).c_str());
 		SetDlgItemText(IDC_EDIT5, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.m_songMessage.GetFormatted(SongMessage::leCRLF)).c_str());
 
 		m_locked = false;
Index: soundlib/Load_it.cpp
===================================================================
--- soundlib/Load_it.cpp	(revision 17650)
+++ soundlib/Load_it.cpp	(working copy)
@@ -1278,6 +1278,19 @@
 	m_modFormat.type = (GetType() == MOD_TYPE_MPT) ? U_("mptm") : U_("it");
 	m_modFormat.madeWithTracker = std::move(madeWithTracker);
 	m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::Charset::Windows1252 : mpt::Charset::CP437;
+#if MPT_TIME_UTC_ON_DISK
+#ifdef MODPLUG_TRACKER
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_TIME_UTC_ON_DISK_VERSION)) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Local;
+#else
+	m_modFormat.timezone = (m_dwLastSavedWithVersion && (m_dwLastSavedWithVersion >= MPT_TIME_UTC_ON_DISK_VERSION)) ? mpt::Date::LogicalTimezone::UTC : mpt::Date::LogicalTimezone::Unspecified;
+#endif
+#else
+#ifdef MODPLUG_TRACKER
+	m_modFormat.timezone = mpt::Date::LogicalTimezone::Local;
+#else
+	m_modFormat.timezone = mpt::Date::LogicalTimezone::Unspecified;
+#endif
+#endif
 
 	return true;
 }
@@ -1359,15 +1372,19 @@
 		} else if(pModDoc != nullptr)
 		{
 			// Current ("new") timestamp
-			const time_t creationTime = pModDoc->GetCreationTime();
-			mptHistory.loadDate = mpt::Date::AnyGregorian{};
-			//localtime_s(&loadDate, &creationTime);
-			const tm* const p = localtime(&creationTime);
-			if (p != nullptr)
-				mptHistory.loadDate = mpt::Date::AsGregorian(*p);
-			else
-				sndFile.AddToLog(LogError, U_("Unable to retrieve current time."));
-			mptHistory.openTime = (uint32)(difftime(time(nullptr), creationTime) * HISTORY_TIMER_PRECISION);
+			const mpt::Date::Unix creationTime = pModDoc->GetCreationTime();
+			if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			} else if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::Local)
+			{
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(creationTime));
+			} else
+			{
+				// assume UTC
+				mptHistory.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(creationTime));
+			}
+			mptHistory.openTime = static_cast<uint32>(mpt::round((mpt::Date::UnixAsSeconds(mpt::Date::UnixNow()) - mpt::Date::UnixAsSeconds(creationTime)) * HISTORY_TIMER_PRECISION));
 #endif // MODPLUG_TRACKER
 		}
 
Index: soundlib/Load_mod.cpp
===================================================================
--- soundlib/Load_mod.cpp	(revision 17650)
+++ soundlib/Load_mod.cpp	(working copy)
@@ -2182,6 +2182,11 @@
 		if(mpt::is_in_range(info.dateMonth, 1, 12) && mpt::is_in_range(info.dateDay, 1, 31) && mpt::is_in_range(info.dateHour, 0, 23)
 		   && mpt::is_in_range(info.dateMinute, 0, 59) && mpt::is_in_range(info.dateSecond, 0, 59))
 		{
+#ifdef MODPLUG_TRACKER
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Local;
+#else
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Unspecified;
+#endif
 			FileHistory mptHistory;
 			mptHistory.loadDate.year = info.dateYear + 1900;
 			mptHistory.loadDate.month = info.dateMonth;
Index: soundlib/Sndfile.cpp
===================================================================
--- soundlib/Sndfile.cpp	(revision 17650)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -57,19 +57,47 @@
 }
 
 
-mpt::ustring FileHistory::AsISO8601() const
+mpt::ustring FileHistory::AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const
 {
-	tm date = mpt::Date::AsTm(loadDate);
 	if(openTime > 0)
 	{
 		// Calculate the date when editing finished.
 		double openSeconds = static_cast<double>(openTime) / HISTORY_TIMER_PRECISION;
-		tm tmpLoadDate = mpt::Date::AsTm(loadDate);
-		int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTCtm(tmpLoadDate));
-		int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
-		date = mpt::Date::UnixAsUTCtm(mpt::Date::UnixFromSeconds(saveDateSinceEpoch));
+		mpt::Date::AnyGregorian tmpLoadDate = loadDate;
+		if (internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::UnixAsLocal(mpt::Date::UnixFromSeconds(saveDateSinceEpoch)));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			// assume UTC for unspecified timezone when calculating
+			int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(tmpLoadDate)));
+			int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
+			return mpt::Date::ToShortenedISO8601(mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch))));
+		}
+	} else
+	{
+		if(internalTimezone == mpt::Date::LogicalTimezone::UTC)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(loadDate));
+#ifdef MODPLUG_TRACKER
+		} else if(internalTimezone == mpt::Date::LogicalTimezone::Local)
+		{
+			return mpt::Date::ToShortenedISO8601(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(loadDate));
+#endif // MODPLUG_TRACKER
+		} else
+		{
+			return mpt::Date::ToShortenedISO8601(loadDate);
+		}
 	}
-	return mpt::Date::ToShortenedISO8601(date);
 }
 
 
@@ -486,8 +514,50 @@
 		InitializeGlobals();
 		m_visitedRows.Initialize(true);
 		m_dwCreatedWithVersion = Version::Current();
+#if MPT_TIME_UTC_ON_DISK
+#ifdef MODPLUG_TRACKER
+		if(GetType() & MOD_TYPE_IT)
+		{
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
+		} else
+		{
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Local;
+		}
+#else // !MODPLUG_TRACKER
+		if (GetType() & MOD_TYPE_IT)
+		{
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
+		} else
+		{
+			m_modFormat.timezone = mpt::Date::LogicalTimezone::Unspecified;
+		}
+#endif // MODPLUG_TRACKER
+#else
+#ifdef MODPLUG_TRACKER
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::Local;
+#else // !MODPLUG_TRACKER
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::Unspecified;
+#endif // MODPLUG_TRACKER
+#endif
 	}
 
+#if MPT_TIME_UTC_ON_DISK
+#ifdef MODPLUG_TRACKER
+	// convert timestamps to UTC
+	if(m_modFormat.timezone == mpt::Date::LogicalTimezone::Local)
+	{
+		for(auto & fileHistoryEntry : m_FileHistory)
+		{
+			if(fileHistoryEntry.HasValidDate())
+			{
+				fileHistoryEntry.loadDate = mpt::Date::forget_timezone(mpt::Date::UnixAsUTC(mpt::Date::UnixFromLocal(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::Local>(fileHistoryEntry.loadDate))));
+			}
+		}
+		m_modFormat.timezone = mpt::Date::LogicalTimezone::UTC;
+	}
+#endif // MODPLUG_TRACKER
+#endif
+
 	// Adjust channels
 	const auto muteFlag = GetChannelMuteFlag();
 	for(CHANNELINDEX chn = 0; chn < MAX_BASECHANNELS; chn++)
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 17650)
+++ soundlib/Sndfile.h	(working copy)
@@ -248,7 +248,7 @@
 	// Time the file was open in the editor, in 1/18.2th seconds (frequency of a standard DOS timer, to keep compatibility with Impulse Tracker easy).
 	uint32 openTime = 0;
 	// Return the date as a (possibly truncated if not enough precision is available) ISO 8601 formatted date.
-	mpt::ustring AsISO8601() const;
+	mpt::ustring AsISO8601(mpt::Date::LogicalTimezone internalTimezone) const;
 	// Returns true if the date component is valid. Some formats only store edit time, not edit date.
 	bool HasValidDate() const
 	{
@@ -286,6 +286,7 @@
 	mpt::ustring originalFormatName; // "FastTracker 2" in the case of converted formats like MO3 or GDM
 	mpt::ustring originalType;       // "xm" in the case of converted formats like MO3 or GDM
 	mpt::Charset charset = mpt::Charset::UTF8;
+	mpt::Date::LogicalTimezone timezone = mpt::Date::LogicalTimezone::Unspecified;
 };
 
 
@@ -753,6 +754,10 @@
 			return GetCharsetFile();
 		#endif // MODPLUG_TRACKER
 	}
+	mpt::Date::LogicalTimezone GetTimezoneInternal() const
+	{
+		return m_modFormat.timezone;
+	}
 
 	ModMessageHeuristicOrder GetMessageHeuristic() const;
 
Index: src/mpt/base/detect_quirks.hpp
===================================================================
--- src/mpt/base/detect_quirks.hpp	(revision 17650)
+++ src/mpt/base/detect_quirks.hpp	(working copy)
@@ -244,13 +244,15 @@
 #define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
 #endif
 #if MPT_LIBCXX_MS && (MPT_MSVC_BEFORE(2022, 2) || !MPT_COMPILER_MSVC)
+#elif MPT_LIBCXX_GNU
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#endif
+#if MPT_LIBCXX_MS && (MPT_MSVC_BEFORE(2022, 3) || !MPT_COMPILER_MSVC)
 // Causes massive memory leaks.
 // See
 // <https://developercommunity.visualstudio.com/t/stdchronoget-tzdb-list-memory-leak/1644641>
 // / <https://github.com/microsoft/STL/issues/2504>.
-#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
-#elif MPT_LIBCXX_GNU
-#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#define MPT_LIBCXX_QUIRK_CHRONO_TZ_MEMLEAK
 #endif
 #endif
 
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 17650)
+++ test/test.cpp	(working copy)
@@ -2741,12 +2741,37 @@
 	// Edit history
 	VERIFY_EQUAL_NONCONT(sndFile.GetFileHistory().size() > 15, true);
 	const FileHistory &fh = sndFile.GetFileHistory().front();
-	VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
+#ifdef MODPLUG_TRACKER
+	if(sndFile.GetTimezoneInternal() == mpt::Date::LogicalTimezone::UTC)
+	{
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).year, 2011);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).month, 6);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).day, 14);
+#if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 21);
+#else
+#if defined(MPT_FALLBACK_TIMEZONE_WINDOWS_HISTORIC)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 21);
+#elif defined(MPT_FALLBACK_TIMEZONE_WINDOWS_CURRENT)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 21);
+#elif defined(MPT_FALLBACK_TIMEZONE_C)
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 22);
+#else
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).hours, 22);
+#endif
+#endif
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).minutes, 8);
+		VERIFY_EQUAL_NONCONT(mpt::Date::forget_timezone(mpt::Date::UnixAsLocal(mpt::Date::UnixFromUTC(mpt::Date::interpret_as_timezone<mpt::Date::LogicalTimezone::UTC>(fh.loadDate)))).seconds, 32);
+	} else
+#endif // MODPLUG_TRACKER
+	{
+		VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
+		VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
+	}
 	VERIFY_EQUAL_NONCONT((uint32)((double)fh.openTime / HISTORY_TIMER_PRECISION), 31);
 
 	// Macros
chrono-v16.patch (33,551 bytes)   
manx

manx

2022-07-09 13:32

administrator   ~0005251

r17651

Issue History

Date Modified Username Field Change
2022-02-23 14:38 manx New Issue
2022-02-23 14:38 manx Status new => assigned
2022-02-23 14:38 manx Assigned To => manx
2022-02-23 14:39 manx Relationship added related to 0001555
2022-02-23 14:39 manx Note Added: 0005113
2022-02-23 14:39 manx File Added: chrono-v8.patch
2022-05-28 05:20 manx Note Added: 0005193
2022-06-04 16:34 manx Note Added: 0005195
2022-06-04 16:34 manx File Added: chrono-v12.patch
2022-06-04 18:25 manx Note Added: 0005196
2022-06-04 18:25 manx File Added: chrono-v13.patch
2022-06-06 08:01 manx Note Added: 0005197
2022-06-06 08:01 manx File Added: chrono-v14.patch
2022-06-07 08:03 manx Note Added: 0005198
2022-06-07 08:03 manx File Added: chrono-v15.patch
2022-06-07 08:20 manx Note Edited: 0005198
2022-07-09 11:56 manx Note Added: 0005250
2022-07-09 11:56 manx File Added: chrono-v16.patch
2022-07-09 13:32 manx Note Added: 0005251
2022-07-09 13:42 manx Relationship added child of 0001614
2022-07-09 13:42 manx Status assigned => resolved
2022-07-09 13:42 manx Resolution open => fixed
2022-07-09 13:42 manx Fixed in Version => OpenMPT 1.31.01.00 / libopenmpt 0.7.0 (upgrade first)