Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ParseTime (strptime) native #1697

Merged
merged 10 commits into from
Jul 4, 2023

Conversation

sirdigbot
Copy link
Contributor

@sirdigbot sirdigbot commented Jan 22, 2022

After discussing in the discord I was told this was a sane idea.
Although unfortunately strptime is not in MSVCRT, so I had to look around for a cross platform workaround (which works, as far as I am able to understand the explanation)

To get it working, I had to include sstream and iomanip into smn_core.cpp. I'm not sure if that's some sort of faux pas or not.

I also grabbed most of the important logic from SO and another site, so I'm not sure of whether or not this can be merged legitimately.

I tested outside of SM with slightly modified code below:

#include <iostream>
#include <iomanip>
#include <sstream>
#include <time.h>

static int DaysFromEpoch(int year, unsigned month, unsigned day)
{
	/* https://howardhinnant.github.io/date_algorithms.html#days_from_civil */
	year -= (month <= 2);
	const int era = (year >= 0 ? year : year-399) / 400; /* C++11 trunc. division */
	const unsigned yearOfEra = static_cast<unsigned>(year - era * 400); /* [0, 399] */
	const unsigned dayOfYear = (153*(month > 2 ? month-3 : month+9) + 2)/5 + day-1; /* [0, 365] */
	const unsigned dayOfEra = yearOfEra * 365 + yearOfEra/4 - yearOfEra/100 + dayOfYear; /* [0, 146096] */
	return era * 146097 + static_cast<int>(dayOfEra) - 719468;
}

//static cell_t ParseTime(IPluginConext *pContext, const cell_t *params)
static int ParseTime(const char *datetime, const char *format)
{
	//char *format, *datetime;
	//pContext->LocalToString(params[1], &datetime);
	//pContext->LocalToStringNULL(params[2], &format);

	//if (format == NULL)
	//{
		//format = const_cast<char *>(bridge->GetCvarString(g_datetime_format));
	//}

	std::tm t;
	std::istringstream input(datetime);
	input.imbue(std::locale(setlocale(LC_TIME, nullptr)));
	input >> std::get_time(&t, format);
	if (input.fail())
	{
		//return pContext->ThrowNativeError("Invalid date/time string or time format.");
		std::cout << "Invalid date/time string or time format." << std::endl;
		return 0;
	}

	/* https://stackoverflow.com/a/58037981 */
	int year = t.tm_year + 1900;
	int month = t.tm_mon; // 0-11
	if (month > 11)
	{
		year += month / 12;
		month %= 12;
	}
	else if (month < 0)
	{
		int yearsDiff = (11 - month) / 12;
		year -= yearsDiff;
		month += 12 * yearsDiff;
	}

	int totalDays = DaysFromEpoch(year, month + 1, t.tm_mday);
	return 60 * (60 * (24L * totalDays + t.tm_hour) + t.tm_min) + t.tm_sec;
}


int main()
{
    char buffer[32];
    int utc;
    time_t currentTime;
    struct tm *ptm;
    
    // Format current time into a string
    time(&currentTime);
    ptm = gmtime(&currentTime);
    strftime(buffer, sizeof buffer, "%Y-%m-%d %H:%M:%S", ptm);
    
    // Get current time from formatted string
    utc = ParseTime(buffer, "%Y-%m-%d %H:%M:%S");
    
    std::cout << "ParseTime(): " << utc << "\ntime(): " << currentTime << std::endl;
}

@sirdigbot
Copy link
Contributor Author

if my code sucks let me know because boy howdy do i not know what I am doing

@sirdigbot sirdigbot changed the title Add GetTimeStamp (strptime) native Add ParseTime (strptime) native Jan 22, 2022
@KyleSanderson
Copy link
Member

Is this able to be implemented in pawn? I'm not sure we need pure 32bit math in a native (I may have missed the call to a stdio function).

@sirdigbot
Copy link
Contributor Author

Is this able to be implemented in pawn? I'm not sure we need pure 32bit math in a native (I may have missed the call to a stdio function).

Not easily. The core of it is std::get_time, which is a cross-platform/c++ version of strptime.
There are a few strptime implementations out there you could adapt into sourcepawn but it would be less of a headache to just use this.

@sirdigbot sirdigbot marked this pull request as draft April 18, 2023 05:53
@sirdigbot
Copy link
Contributor Author

sirdigbot commented Apr 18, 2023

Rewrite seems to work only sometimes.
Tested on godbolt and ingame and it worked fine but testing the same code in visual studio seems to break.

Seems std::get_time either outputs global or local time entirely dependent on platform (note the hour)?
image
image

@KyleSanderson KyleSanderson marked this pull request as ready for review April 18, 2023 07:44
@sirdigbot
Copy link
Contributor Author

Ok so std::get_time wasn't the problem. FormatTime was.

Because I had forgotten that FormatTime uses local time, my old code never actually worked properly and my testing was giving me incorrect results.
That's been hopefully-for-real-this-time fixed.

I also fixed the locale being changed, a concern that headline had brought up that I didn't really understand at the time.

I've tested the new code on Godbolt/GCC and in Visual Studio with:

#include <time.h>
#include <iomanip>
#include <sstream>

#include <iostream>

#define PLATFORM_WINDOWS

//static int ParseTime(IPluginContext* pContext, const cell_t* params)
static int ParseTime(const char* datetime, const char* format)
{
	//char* datetime;
	//char* format;
	//pContext->LocalToStringNULL(params[1], &datetime);
	//pContext->LocalToStringNULL(params[2], &format);

	//if (format == NULL)
	//{
	//	format = const_cast<char*>(bridge->GetCvarString(g_datetime_format));
	//}
	//else if (!format[0])
	//{
	//	return pContext->ThrowNativeError("Time format string cannot be empty.");
	//}
	//if (!datetime || !datetime[0])
	//{
	//	return pContext->ThrowNativeError("Date/time string cannot be empty.");
	//}

	// https://stackoverflow.com/a/33542189
	std::tm t{};
	std::istringstream input(datetime);

	auto previousLocale = input.imbue(std::locale::classic());
	input >> std::get_time(&t, format);
	bool failed = input.fail();
	input.imbue(previousLocale);

	if (failed)
	{
		//return pContext->ThrowNativeError("Invalid date/time string or time format.");
		return -1;
	}

	std::cout << "std::get_time data:\n"
		<< "\ttm_sec:" << t.tm_sec   // seconds after the minute - [0, 60] including leap second
		<< "\n\ttm_min:" << t.tm_min   // minutes after the hour - [0, 59]
		<< "\n\ttm_hour:" << t.tm_hour  // hours since midnight - [0, 23]
		<< "\n\ttm_mday:" << t.tm_mday  // day of the month - [1, 31]
		<< "\n\ttm_mon:" << t.tm_mon   // months since January - [0, 11]
		<< "\n\ttm_year:" << t.tm_year  // years since 1900
		<< "\n\ttm_wday:" << t.tm_wday  // days since Sunday - [0, 6]
		<< "\n\ttm_yday:" << t.tm_yday  // days since January 1 - [0, 365]
		<< "\n\ttm_isdst:" << t.tm_isdst // daylight savings time flag
		<< std::endl;

#if defined PLATFORM_WINDOWS
	return _mkgmtime(&t);
#elif defined PLATFORM_LINUX || defined PLATFORM_APPLE
	return timegm(&t);
#else
	//return pContext->ThrowNativeError("Platform has no supported UTC conversion for std::tm to std::time_t");
	return -1;
#endif
}

int main()
{
	char buffer[32];
	int utc;
	struct tm* ptm;

	// Format current time into a string
	time_t currentTime = time(NULL);
	ptm = gmtime(&currentTime);
	strftime(buffer, sizeof buffer, "%Y-%m-%d %H:%M:%S", ptm);

	// Get current time from formatted string
	utc = ParseTime(buffer, "%Y-%m-%d %H:%M:%S");
	std::cout << "ParseTime(): " << utc << "\ntime(): " << currentTime << std::endl;
}

And tested it in sourcemod on windows with

public void OnPluginStart()
{
    // FormatTime uses localtime, we need UTC.
    // 1600000000 unix time = Sunday, September 13, 2020 12:26:40 PM
    int parsedNow = ParseTime("2020-09-13 12:26:40", "%Y-%m-%d %H:%M:%S");

    PrintToServer("UTC: 1600000000\nParseTime: %i", parsedNow);
}

Copy link
Member

@peace-maker peace-maker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on hl2sdk-mock and it appears to work fine. Thank you and sorry for the delay!

It might be useful to make the timezone explicit since it might not be obvious that unix timestamps are always UTC?

@sirdigbot
Copy link
Contributor Author

It might be useful to make the timezone explicit since it might not be obvious that unix timestamps are always UTC?

I'm assuming you mean in the docstring?

Would adding an offset parameter make sense since too, since presumably you're more likely to have a non-UTC datetime string (given that FormatTime currently doesn't have a UTC parameter)?

@peace-maker
Copy link
Member

Yes, I was talking about the documentation. I'd rather allow control over the output of FormatTime than including an offset here.

@sirdigbot
Copy link
Contributor Author

I realised that actually you could just offset the result of ParseTime for the same effect anyway

@peace-maker peace-maker merged commit 5addaff into alliedmodders:master Jul 4, 2023
@sirdigbot sirdigbot deleted the date-to-unix-time branch August 6, 2023 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants