Glk time and date functions

I’ve implemented my planned date-and-time functions in C and Javascript, so I’m satisfied that they’re possible. :slight_smile:

The spec for these functions is visible at github.com/erkyrath/glk-dev/wik … ec-changes

(I’ve added some details since I first posted them.)

The only possibly annoying corner is Daylight Saving Time. (It always is.) To make the local-time back-and-forth conversions work sanely (converting a date-and-time to a timestamp and back), I wrote that the library should try to guess whether DST is in effect. This is what Javascript’s Date class does, and it’s what libc’s mktime() function does if you set tm_isdst to -1. But you can’t be sure that it will guess right. This is something that people put up with, as far as I can tell.

There is no explicit function to determine your current time zone. But you can work it out converting the current timestamp to a local date, converting it back as UTC, and then comparing. (There’s some test code to do this: eblong.com/zarf/glulx/datetimetest.inf )

If anybody has any further questions, now is the time to raise them…

If you’re considering allowing the use of past values, you might specify whether weird things are to be expected in 1752 (or another year, depending on locale).

Did you consider adding 64-bit add/subtract/multiply/divide instructions to glulx instead of the “simple” half of the time functions?

add64 L1 L2 L3 L4 S1 S2
adds the 64-bit number represented by ((L1 << 32) | L2) to ((L3 << 32) | L4) and stores into S1 and S2

sub64 L1 L2 L3 L4 S1 S2
subtracts the 64-bit number represented by ((L3 << 32) | L4) from ((L1 << 32) | L2) and stores into S1 and S2

mul64 L1 L2 L3 L4 S1 S2
multiplies the signed 64-bit number represented by ((L1 << 32) | L2) by ((L3 << 32) | L4) and stores into S1 and S2

div64 L1 L2 L3 L4 S1 S2
divides the signed 64-bit number represented by ((L1 << 32) | L2) by ((L3 << 32) | L4) and stores into S1 and S2

I assume 0 should be returned for the microseconds. For the second time this sentence appears (for the “simple” case), some clarification is needed. Does it store -1 in both seconds fields and then divide? So if my factor is 60, it returns 0x44444444?

I thought about 64-bit values but, as with floats last year, the prospect hurt too much to be worthwhile. The time calculations in my test code only required subtraction of timevals, which is easy enough without 64-bit support in the VM.

The value for microseconds is undefined in the error case.

Sorry, that’s another cut-paste error. For the simple case, it should read: “If the time cannot be represented by the platform’s time library, this may return -1.” (This is easy to arrange: -1 divided by any positive factor, rounded towards negative infinity, is -1.)

I’ve released the spec, CheapGlk, and Quixe. (I’ll get to GlkTerm tonight.)

eblong.com/zarf/glk/
eblong.com/zarf/glulx/quixe/

To try the date-time features in Quixe: eblong.com/zarf/glulx/quixe/quix … est.ulx.js

I had to adjust the glk_date_to_time_utc() and glk_date_to_simple_time_utc() in cgdate.c to deal with some DST weirdness on Windows when using the tzset() function. I borrowed the tm.tm_isdst = -1; line from the local time version.

void glk_date_to_time_utc(glkdate_t *date, glktimeval_t *time)
{
    time_t timestamp;
    struct tm tm;
    glsi32 microsec;

    microsec = gli_date_to_tm(date, &tm);
    tm.tm_isdst = -1;    // let mktime call in timegm() determine DST
    timestamp = timegm(&tm);

    gli_timestamp_to_time(timestamp, microsec, time);
}

I’m still getting errors for the Moon landing, but it’s a start.

Hm. My (Darwin) man page for mktime() says: “The tm_isdst and tm_gmtoff members are forced to zero by timegm().” Does it work better if you set that field to 0 instead of -1 before calling timegm()?

I had to roll my own timegm() function, and I didn’t realize that the tm_isdst field wouldn’t simply be ignored when TZ was set to UTC.

Thanks for the man page excerpt; I’ll try zeroing out tm_gmtoff as well.

Microsoft’s implementation of mktime has the following behavior:

This applies to all versions of Windows, and affects Linux as well, though because Linux has a timegm() function that doesn’t rely on mktime(), the error doesn’t show up in your test. The BSD mktime() will yield negative time_t values but it looks like the odd man out.

Can you add tests for calling glk_date_to_time_local() and glk_date_to_simple_time_local() with pre-Epoch dates to datetimetest.inf?

Do we know if any of these new Glulx features will be standardized in Inform 7? I ask because, if not, I’ll be happy to write small extensions exposing the functionality for authors. But if I7 itself will support them directly I don’t wish to duplicate work.

Also: yay zarf!

The gizmo will do this if you “set year to 1969”. Does that do what you expect?

I didn’t realize that BSD was different from Linux. Hm. I’ll scrounge up a Linux account and do some testing. I agree that this needs at least a special test, and a note in the spec as well.

I haven’t heard. But I see nothing wrong with writing an extension now. If a future I7 release makes it irrelevant, no big deal.

“set gizmo to 1969” works on Ubuntu x64. Most of the complaints re: mktime() on Linux seem to center on Red Hat / Fedora (glibc). Debian has since switched to eglibc and may handle things differently.

I will download Fedora and test.

EDIT:

Tested it on Fedora x86 and it works just fine with years between 1901 and 2038. Huh.

Yeah, the Debian box I tried worked from 1902 to 2037. (It had the 32-bit warnings on the calendar.)

From the threads you linked to, it sounds like we can treat the Red Hat behavior as a bug in a small number of versions, in practice.

In case anyone finds it helpful, I added wrappers to mktime and friends on Windows to handle dates before the epoch.

Good.

I’ll copy those WIN32 patches into the cheapglk distro, if that’s okay with you.

That’s fine with me.

I’ve got the Inform 7 code up & running, and will wrap it up as an extension soon. Any suggestions, now’s the time.

[spoiler][code]“Player Time”

There is room.

When play begins, unless the player’s time is available, say “Your interpreter does not support this feature. Try the Quixe interpreter from http://eblong.com/zarf/glulx/quixe/ either online, or, downloaded and installed and using the following line of code.[paragraph break]Release along with the ‘Quixe’ interpreter.”

When play begins when the player’s time is available, say “It’s now [the player’s time] on [player’s weekday], [player’s month] [player’s day], [player’s year]. Your timezone is [the player’s timezone].”

Release along with the “Quixe” interpreter.

Represent time locally is a truth state that varies. The represent time locally variable translates into I6 as “UTCvsLocal”. [Represent time locally is usually true.]

A weekday is a kind of value. The weekdays are Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday.

A month is a kind of value. The months are January, February, March, April, May, June, July, August, September, October, November, December.

To decide if the player’s time is available: (- (glk($0004, 20, 0)) -). [! gestalt_DateTime]

To decide what number is the player’s year: (- ((FillOutDateTimeArrays() & datetime–>0)) -).
To decide what month is the player’s month: (- ((FillOutDateTimeArrays() & datetime–>1)) -).
To decide what number is the player’s day: (- ((FillOutDateTimeArrays() & datetime–>2)) -).
To decide what weekday is the player’s weekday: (- ((FillOutDateTimeArrays() & datetime–>3) + 1) -).
[To decide what time is the player’s time: (- ((FillOutDateTimeArrays() & ((60 * datetime–>4) + datetime–>5))) -).]
To decide what time is the player’s time: (- ((GetPlayersTime())) -).
To decide what number is the player’s seconds: (- ((FillOutDateTimeArrays() & datetime–>6)) -).
To decide what number is the player’s microseconds: (- ((FillOutDateTimeArrays() & datetime–>7)) -).

To decide what number is the player’s timezone: (- GetPlayersTimezone() -).

Include (-
Array countofseconds --> 4; ! this holds the number of microseconds elapsed since midnight on January 1, 1970, GMT/UTC
Array datetime --> 9; ! this holds the above broken down into year, month, day, weekday, hour, minute, second, and microseconds
Global UTCvsLocal = 1; ! truth state: 1 = local time; 0 = GMT otherwise known as UTC

[ FillOutDateTimeArrays;
if (~~(glk($0004, 20, 0))) return 0; ! glk_gestalt_DateTime(20, 0);
glk($0160, countofseconds); ! glk_current_time(timeval);
if (UTCvsLocal) glk($0169, countofseconds, datetime); ! glk_time_to_date_local(tv, date);
else glk($0168, countofseconds, datetime); ! glk_time_to_date_utc(tv, date);
return -1; ! so this function can be bitwise ANDed with the result of the --> array dereference that’s about to happen
];

[ GetPlayersTime;
FillOutDateTimeArrays();
return (60 * datetime–>4) + datetime–>5;
];

[ GetPlayersTimezone zonesec;
if (~~(glk($0004, 20, 0))) return 0; ! glk_gestalt_DateTime(20, 0);
glk($0160, countofseconds); ! current_time
glk($0169, countofseconds, datetime); ! time_to_date_local
zonesec = countofseconds–>1;
glk($016C, datetime, countofseconds); ! date_to_time_utc
return (countofseconds–>1 - zonesec) / 3600; ! UTC - local
];
-) after “Definitions.i6t”.
[/code][/spoiler]

You have the countofseconds and datetime arrays defined as 4 and 9 words. These only need to be 3 and 8. (I use 4 and 9 in the unit test so that I can add a guard value against interpreter bugs, but the interpreters are passing that test.)

A timezone offset isn’t necessarily an even number of hours. In some countries it’s a half-hour value.

The way you’re giving access to the year/month/day variables isn’t guaranteed to be consistent. Since glk_current_time() gets called multiple times, the game could conceivably print “January 1” at the beginning of February, if it’s run just at midnight. I realize this is unlikely, but it’s worth thinking about ways for the game to handle complete time values.

I don’t think user code can create multi-word I7 structures. However, you could define a time scalar – simple-time in minutes seems appropriate, since the native I7 time representation is in minutes. Or you could define a kind which includes an eight-word property:

Include (-
  with datetimeprop 0 0 0 0 0 0 0 0,
-) when defining a datetime.

Ah, excellent.

Doh. OK, it now returns type time rather than type number, and I taught Inform how to print negative time. Oh wait, positive time will have AM/PM on it, won’t it. Grr…

Hrm. Well I was ready to handoff the problem of a player playing on a laptop or cell while travelling & crossing time zones to the player anyway. But OK. I’ve added while suspending time which works like an if-statement: it updates time as its code block begins, but calls to the various player’s time/day/month/etc functions within do not re-get the time. If that sounds like an odd way to do it, well, I am presuming far more bugs would result from requiring the writer to remember to call “update time” or somesuch every time he uses a player’s time variable. Doing it this way seems easier for game authors: if they don’t care about edge cases and the like, the extension is easy to use as including it and using a variable. If they do care about it, it’s easy to use said variables within a suspending-time block.

I really didn’t want to make an object and class for this piddling thing.

When play begins when the player's time is available: while suspending time: say "Your time is [the player's time]."; say "Your date is [player's weekday], [player's month] [player's day], [player's year]."; say "Oh, and your timezone is [the player's timezone]."; say line break; while suspending time, say "It's [player's weekday], [player's month] [player's day], [player's year]."

I’ll poke the timezone thing some more. Is it possible it returns a minute value that isn’t exactly :00 or :30? (My code there now more closely resembles yours.)

(Code in total so far:)[spoiler][code]“Player Time”

There is room.

When play begins, unless the player’s time is available, say “Your interpreter does not support this feature. Try the Quixe interpreter from Quixe: a Glulx VM interpreter written in Javascript either online, or, downloaded and installed and using the following line of code.[paragraph break]Release along with the ‘Quixe’ interpreter.”

When play begins when the player’s time is available, say “It’s now [the player’s time] on [player’s weekday], [player’s month] [player’s day], [player’s year]. Your timezone is [the player’s timezone].”

Release along with the “Quixe” interpreter.

Represent time locally is a truth state that varies. The represent time locally variable translates into I6 as “UTCvsLocal”. [Represent time locally is usually true.]

A weekday is a kind of value. The weekdays are Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday.

A month is a kind of value. The months are January, February, March, April, May, June, July, August, September, October, November, December.

To decide if the player’s time is available: (- (glk($0004, 20, 0)) -). [! gestalt_DateTime]

To decide what number is the player’s year: (- ((FillOutDateTimeArrays() & datetime–>0)) -).
To decide what month is the player’s month: (- ((FillOutDateTimeArrays() & datetime–>1)) -).
To decide what number is the player’s day: (- ((FillOutDateTimeArrays() & datetime–>2)) -).
To decide what weekday is the player’s weekday: (- ((FillOutDateTimeArrays() & datetime–>3) + 1) -).
[To decide what time is the player’s time: (- ((FillOutDateTimeArrays() & ((60 * datetime–>4) + datetime–>5))) -).]
To decide what time is the player’s time: (- ((GetPlayersTime())) -).
To decide what number is the player’s seconds: (- ((FillOutDateTimeArrays() & datetime–>6)) -).
To decide what number is the player’s microseconds: (- ((FillOutDateTimeArrays() & datetime–>7)) -).

To decide what time is the player’s timezone: (- GetPlayersTimezone() -).

Include (-
Array countofseconds → 3; ! this holds the number of microseconds elapsed since midnight on January 1, 1970, GMT/UTC
Array datetime → 8; ! this holds the above broken down into year, month, day, weekday, hour, minute, second, and microseconds
Global UTCvsLocal = 1; ! truth state: 1 = local time; 0 = GMT otherwise known as UTC
Global suspend_glk_get_time = 0; ! -1 = suspended; 0 = not suspended

[ FillOutDateTimeArrays;
if (~~(glk($0004, 20, 0))) return 0; ! glk_gestalt_DateTime(20, 0);
if (suspend_glk_get_time == -1) return -1;
glk($0160, countofseconds); ! glk_current_time(timeval);
if (UTCvsLocal) glk($0169, countofseconds, datetime); ! glk_time_to_date_local(tv, date);
else glk($0168, countofseconds, datetime); ! glk_time_to_date_utc(tv, date);
return -1; ! so this function can be bitwise ANDed with the result of the → array dereference that’s about to happen
];

[ GetPlayersTime;
FillOutDateTimeArrays();
return (60 * datetime–>4) + datetime–>5;
];

[ GetPlayersTimezone zonesec zonemin zonehour firstsecondcount;
if (~~(glk($0004, 20, 0))) return 0; ! glk_gestalt_DateTime(20, 0);
glk($0160, countofseconds); ! glk_current_time()
glk($0169, countofseconds, datetime); ! glk_time_to_date_local()
firstsecondcount = countofseconds–>1;
glk($016C, datetime, countofseconds); ! glk_date_to_time_utc()
zonesec = (countofseconds–>1 - firstsecondcount); ! UTC - local
zonehour = zonesec / 3600;
zonesec = zonesec % 3600;
zonemin = zonesec / 60;
return (zonehour * 60) + zonemin;
];
-) after “Definitions.i6t”.

Definition: a time is positive rather than negative if it is at least zero minutes.

To say (T - a negative time):
if T < 0 minutes:
say “-”;
now T is 0 minutes minus T;
let H be the hours part of T;
let M be the minutes part of T;
say “[H][unless M is zero]:[M]”

To while suspending time begin – end: (- for ( suspend_glk_get_time = FillOutDateTimeArrays() : suspend_glk_get_time == -1 : suspend_glk_get_time++) -).

To while suspending time, (ph - a phrase): (- for ( suspend_glk_get_time = FillOutDateTimeArrays() : suspend_glk_get_time == -1 : suspend_glk_get_time++) {ph}; -).

When play begins when the player’s time is available:
while suspending time:
say “Your time is [the player’s time].”;
say “Your date is [player’s weekday], [player’s month] [player’s day], [player’s year].”;
say “Oh, and your timezone is [the player’s timezone].”;
say line break;
while suspending time, say “It’s now [the player’s time] on [player’s weekday], [player’s month] [player’s day], [player’s year]. Your timezone is [the player’s timezone].”
[/code][/spoiler]

Oh, and is there anything to add that might be useful to I7 authors trying to profile parts of their code? I exposed microseconds only for that reason, but couldn’t think if anything else would be handy.

Thanks for putting this together, Ron!

I think that one of the potential uses of clock time will be to resync the timer, so that the game can keep relatively accurate time even with a long-fuse timer. How well suited are your current wrappers to that purpose? (Sorry, I’m not savvy enough to follow the discussion with all the mktimers and opcodes and all that, and I’m at work where I have an old Quixe installation w/o these magic powers…)

I also want to investigate the potential for piggybacking on clock time to provide named triggers for real-time events. (In other words, use a T + timer model to check the time when a timer event fires and return a specific named event.)

In both cases, I guess what would be easiest is a number-type representing milliseconds.

–Erik