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