Port date/time functions to GDateTime
[projects/chimara/chimara.git] / libchimara / datetime.c
1 #include <glib.h>
2 #include "glk.h"
3 #include "magic.h"
4
5 /* Parts adapted from Andrew Plotkin's Cheapglk implementation */
6
7 /* Copy a GDateTime to a glkdate. */
8 static void
9 date_from_gdatetime(glkdate_t *date, GDateTime *dt)
10 {
11     date->year = g_date_time_get_year(dt);
12     date->month = g_date_time_get_month(dt);
13     date->day = g_date_time_get_day_of_month(dt);
14     /* GDateTime has 1-7, with 1 = Monday; Glk has 0-6, with 0 = Sunday */
15     date->weekday = g_date_time_get_day_of_week(dt) % G_DATE_SUNDAY;
16     date->hour = g_date_time_get_hour(dt);
17     date->minute = g_date_time_get_minute(dt);
18     date->second = g_date_time_get_second(dt);
19 }
20
21 /* Copy a glkdate to a GDateTime.
22  This is used in the "glk_date_to_..." functions, which are supposed
23  to normalize the glkdate. We're going to rely on GDateTime to do that.
24  Returns NULL if the date is not supported by GDateTime.
25  Call g_date_time_unref() on the return value when done.
26  */
27 static GDateTime *
28 date_to_gdatetime(glkdate_t *date, GTimeZone *tz)
29 {
30         /* Combining seconds and microseconds into one floating-point number should
31          * take care of normalizing any negative microseconds or microseconds > one
32          * million */
33         double seconds = date->second + (double)date->microsec / G_USEC_PER_SEC;
34         GDateTime *retval = g_date_time_new(tz,
35                 date->year,
36                 date->month,
37                 date->day,
38                 date->hour,
39                 date->minute,
40                 seconds);
41         if( G_UNLIKELY(retval == NULL) ) {
42                 if(date->year < 1)
43                         WARNING("Years earlier than 1 C.E. are not currently supported.");
44                 else if(date->year > 9999)
45                         WARNING("Years later than 9999 C.E. are not currently supported.");
46                 else
47                         WARNING("Date is not supported or not valid.");
48         }
49         return retval;
50 }
51
52 /* Convert a Unix timestamp (seconds since Jan 1 1970) to a glktimeval,
53  * adding a number of microseconds as well. */
54 static void
55 unix_time_to_time(gint64 sec, int microsec, glktimeval_t *time)
56 {
57         time->high_sec = (sec >> 32) & 0xFFFFFFFF;
58         time->low_sec = sec & 0xFFFFFFFF;
59     time->microsec = microsec;
60 }
61
62 /* Convert a gint64 microseconds value, as returned by g_get_real_time(),
63  * to a glktimeval. */
64 static void
65 real_time_to_time(gint64 real_time, glktimeval_t *time)
66 {
67         gint64 unix_time = real_time / G_USEC_PER_SEC;
68         int microsec = real_time % G_USEC_PER_SEC;
69         unix_time_to_time(unix_time, microsec, time);
70 }
71
72 /* Divide a Unix timestamp by a (positive) value. */
73 static glsi32
74 simplify_time(gint64 timestamp, glui32 factor)
75 {
76     /* We want to round towards negative infinity, which takes a little
77          bit of fussing. */
78     if (timestamp >= 0) {
79                 return timestamp / (gint64)factor;
80     }
81     else {
82                 return -1 - (((gint64)-1 - timestamp) / (gint64)factor);
83     }
84 }
85
86 /* Convert a glkdate to a glktimeval, in the given time zone. */
87 static void
88 date_to_time(glkdate_t *date, glktimeval_t *tv, GTimeZone *tz)
89 {
90         GDateTime *dt = date_to_gdatetime(date, tz);
91         if(dt == NULL) {
92                 tv->high_sec = -1;
93                 tv->low_sec = -1;
94                 return;
95         }
96         gint64 timestamp = g_date_time_to_unix(dt);
97         int microsec = g_date_time_get_microsecond(dt);
98         g_date_time_unref(dt);
99         unix_time_to_time(timestamp, microsec, tv);
100 }
101
102 /* Convert a glkdate to a Unix timestamp divided by a value, in the given time
103 zone. */
104 static glsi32
105 date_to_simple_time(glkdate_t *date, glui32 factor, GTimeZone *tz)
106 {
107         GDateTime *dt = date_to_gdatetime(date, tz);
108         if(dt == NULL)
109                 return -1;
110         gint64 timestamp = g_date_time_to_unix(dt);
111         g_date_time_unref(dt);
112
113         return simplify_time(timestamp, factor);
114 }
115
116 /**
117  * glk_current_time:
118  * @time: pointer to a #glktimeval_t structure.
119  *
120  * The current Unix time is stored in the structure @time. (The argument may not
121  * be %NULL.) This is the number of seconds since the beginning of 1970 (UTC).
122  *
123  * The first two values in the structure should be considered a single
124  * <emphasis>signed</emphasis> 64-bit number. This allows the #glktimeval_t to
125  * store a reasonable range of values in the future and past. The @high_sec
126  * value will remain zero until sometime in 2106. If your computer is running in
127  * 1969, perhaps due to an unexpected solar flare, then @high_sec will be
128  * negative.
129  * 
130  * The third value in the structure represents a fraction of a second, in
131  * microseconds (from 0 to 999999). The resolution of the glk_current_time()
132  * call is platform-dependent; the @microsec value may not be updated
133  * continuously.
134  */
135 void 
136 glk_current_time(glktimeval_t *time)
137 {
138         g_return_if_fail(time != NULL);
139         real_time_to_time(g_get_real_time(), time);
140 }
141
142 /**
143  * glk_current_simple_time:
144  * @factor: Factor by which to divide the time value.
145  *
146  * If dealing with 64-bit values is awkward, you can also get the current time
147  * as a lower-resolution 32-bit value. This is simply the Unix time divided by
148  * the @factor argument (which must not be zero). For example, if factor is 60,
149  * the result will be the number of minutes since 1970 (rounded towards negative
150  * infinity). If factor is 1, you will get the Unix time directly, but the value
151  * will be truncated starting some time in 2038.
152  *
153  * Returns: Unix time divided by @factor, truncated to 32 bits.
154  */
155 glsi32
156 glk_current_simple_time(glui32 factor)
157 {
158         g_return_val_if_fail(factor != 0, 0);
159
160         gint64 sec = g_get_real_time() / G_USEC_PER_SEC;
161         return simplify_time(sec, factor);
162 }
163
164 /**
165  * glk_time_to_date_utc:
166  * @time: A #glktimeval_t structure as returned by glk_current_time().
167  * @date: An empty #glkdate_t structure to fill in.
168  *
169  * Convert the given timestamp (as returned by glk_current_time()) to a
170  * broken-out structure. This function returns a date and time in universal time
171  * (GMT).
172  *
173  * <note><para>
174  *   The seconds value may be 60 because of a leap second.
175  * </para></note>
176  */
177 void
178 glk_time_to_date_utc(glktimeval_t *time, glkdate_t *date)
179 {
180         g_return_if_fail(time != NULL);
181         g_return_if_fail(date != NULL);
182
183         time_t timestamp = time->low_sec;
184     if (sizeof(timestamp) > 4) {
185         timestamp += ((gint64)time->high_sec << 32);
186     }
187
188         GDateTime *dt = g_date_time_new_from_unix_utc(timestamp);
189         date_from_gdatetime(date, dt);
190         g_date_time_unref(dt);
191     date->microsec = time->microsec;
192 }
193
194 /**
195  * glk_time_to_date_local:
196  * @time: A #glktimeval_t structure as returned by glk_current_time().
197  * @date: An empty #glkdate_t structure to fill in.
198  *
199  * Does the same thing as glk_time_to_date_utc(), but this function returns
200  * local time.
201  */
202 void
203 glk_time_to_date_local(glktimeval_t *time, glkdate_t *date)
204 {
205         g_return_if_fail(time != NULL);
206         g_return_if_fail(date != NULL);
207
208         time_t timestamp = time->low_sec;
209     if (sizeof(timestamp) > 4) {
210         timestamp += ((int64_t)time->high_sec << 32);
211     }
212
213         GDateTime *dt = g_date_time_new_from_unix_local(timestamp);
214         date_from_gdatetime(date, dt);
215         g_date_time_unref(dt);
216     date->microsec = time->microsec;
217 }
218
219 /**
220  * glk_simple_time_to_date_utc:
221  * @time: Timestamp as returned by glk_current_simple_time().
222  * @factor: Factor by which to multiply @time in order to get seconds.
223  * @date: An empty #glkdate_t structure to fill in.
224  *
225  * Convert the given timestamp (as returned by glk_current_simple_time()) to a
226  * broken-out structure in universal time. The @time argument is multiplied by
227  * @factor to produce a Unix timestamp.
228  *
229  * Since the resolution of glk_simple_time_to_date_utc() and
230  * glk_simple_time_to_date_local() is no better than seconds, they will return
231  * zero for the microseconds value. 
232  */ 
233 void
234 glk_simple_time_to_date_utc(glsi32 time, glui32 factor, glkdate_t *date)
235 {
236         g_return_if_fail(factor != 0);
237         g_return_if_fail(date != NULL);
238
239         time_t timestamp = (time_t)time * factor;
240
241         GDateTime *dt = g_date_time_new_from_unix_utc(timestamp);
242         date_from_gdatetime(date, dt);
243         g_date_time_unref(dt);
244     date->microsec = 0;
245 }
246
247 /**
248  * glk_simple_time_to_date_local:
249  * @time: Timestamp as returned by glk_current_simple_time().
250  * @factor: Factor by which to multiply @time in order to get seconds.
251  * @date: An empty #glkdate_t structure to fill in.
252  *
253  * Does the same thing as glk_simple_time_to_date_utc(), but fills in the @date
254  * structure in local time.
255  */
256 void
257 glk_simple_time_to_date_local(glsi32 time, glui32 factor, glkdate_t *date)
258 {
259         g_return_if_fail(factor != 0);
260         g_return_if_fail(date != NULL);
261
262         time_t timestamp = (time_t)time * factor;
263
264         GDateTime *dt = g_date_time_new_from_unix_local(timestamp);
265         date_from_gdatetime(date, dt);
266         g_date_time_unref(dt);
267     date->microsec = 0;
268 }
269
270 /**
271  * glk_date_to_time_utc:
272  * @date: A date in the form of a #glkdate_t structure.
273  * @time: An empty #glktimeval_t structure to fill in.
274  *
275  * Convert the broken-out structure (interpreted as universal time) to a
276  * timestamp. The weekday value in @date is ignored. The other values need not
277  * be in their normal ranges; they will be normalized.
278  *
279  * If the time cannot be represented by the platform's time library, this may
280  * return -1 for the seconds value. (I.e., the @high_sec and @low_sec fields
281  * both $FFFFFFFF. The microseconds field is undefined in this case.)
282  * <note><title>Chimara</title><para>
283  *   Chimara does not currently support years earlier than 1 C.E. or later than
284  *   9999 C.E.
285  * </para></note>
286  */
287 void
288 glk_date_to_time_utc(glkdate_t *date, glktimeval_t *time)
289 {
290         g_return_if_fail(date != NULL);
291         g_return_if_fail(time != NULL);
292
293         GTimeZone *utc = g_time_zone_new_utc();
294         date_to_time(date, time, utc);
295         g_time_zone_unref(utc);
296 }
297
298 /**
299  * glk_date_to_time_local:
300  * @date: A date in the form of a #glkdate_t structure.
301  * @time: An empty #glktimeval_t structure to fill in.
302  *
303  * Does the same thing as glk_date_to_time_utc(), but interprets the broken-out
304  * structure as local time.
305  *
306  * The glk_date_to_time_local() function may not be smart about Daylight Saving
307  * Time conversions.
308  * <note><para>
309  *   If implemented with the mktime() libc function, it should use the negative
310  *   @tm_isdst flag to <quote>attempt to divine whether summer time is in
311  *   effect</quote>.
312  * </para></note>
313  */
314 void
315 glk_date_to_time_local(glkdate_t *date, glktimeval_t *time)
316 {
317         g_return_if_fail(date != NULL);
318         g_return_if_fail(time != NULL);
319
320         GTimeZone *local = g_time_zone_new_local();
321         date_to_time(date, time, local);
322         g_time_zone_unref(local);
323 }
324
325 /**
326  * glk_date_to_simple_time_utc:
327  * @date: A date in the form of a #glkdate_t structure.
328  * @factor: Factor by which to divide the time value.
329  *
330  * Convert the broken-out structure (interpreted as universal time) to a
331  * timestamp divided by @factor. The weekday value in @date is ignored. The
332  * other values need not be in their normal ranges; they will be normalized.
333  *
334  * If the time cannot be represented by the platform's time library, this may
335  * return -1.
336  * <note><title>Chimara</title><para>
337  *   Chimara does not currently support years earlier than 1 C.E. or later than
338  *   9999 C.E.
339  * </para></note>
340  *
341  * Returns: a timestamp divided by @factor, and truncated to 32 bits, or -1 on
342  * error.
343  */
344 glsi32
345 glk_date_to_simple_time_utc(glkdate_t *date, glui32 factor)
346 {
347         g_return_val_if_fail(date != NULL, 0);
348         g_return_val_if_fail(factor != 0, 0);
349
350         GTimeZone *utc = g_time_zone_new_utc();
351         glsi32 retval = date_to_simple_time(date, factor, utc);
352         g_time_zone_unref(utc);
353         return retval;
354 }
355
356 /**
357  * glk_date_to_simple_time_local:
358  * @date: A date in the form of a #glkdate_t structure.
359  * @factor: Factor by which to divide the time value.
360  *
361  * Does the same thing as glk_date_to_simple_time_utc(), but interprets the
362  * broken-out structure as local time.
363  *
364  * Returns: a timestamp divided by @factor, and truncated to 32 bits, or -1 on
365  * error.
366  */
367 glsi32
368 glk_date_to_simple_time_local(glkdate_t *date, glui32 factor)
369 {
370         g_return_val_if_fail(date != NULL, 0);
371         g_return_val_if_fail(factor != 0, 0);
372
373         GTimeZone *local = g_time_zone_new_local();
374         glsi32 retval = date_to_simple_time(date, factor, local);
375         g_time_zone_unref(local);
376         return retval;
377 }