gstdatetime.c 28.9 KB
Newer Older
Thiago Santos's avatar
Thiago Santos committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* GStreamer
 * Copyright (C) 2010 Thiago Santos <thiago.sousa.santos@collabora.co.uk>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
Tim-Philipp Müller's avatar
Tim-Philipp Müller committed
16
17
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
Thiago Santos's avatar
Thiago Santos committed
18
19
20
21
22
23
24
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "gst_private.h"
25
#include "glib-compat-private.h"
Thiago Santos's avatar
Thiago Santos committed
26
#include "gstdatetime.h"
27
#include "gstvalue.h"
28
#include <glib.h>
29
#include <math.h>
30
#include <stdio.h>
31

Thiago Santos's avatar
Thiago Santos committed
32
/**
33
 * SECTION:gstdatetime
Thiago Santos's avatar
Thiago Santos committed
34
35
36
37
38
39
 * @title: GstDateTime
 * @short_description: A date, time and timezone structure
 *
 * Struct to store date, time and timezone information altogether.
 * #GstDateTime is refcounted and immutable.
 *
40
 * Date information is handled using the [proleptic Gregorian calendar].
Thiago Santos's avatar
Thiago Santos committed
41
42
 *
 * Provides basic creation functions and accessor functions to its fields.
43
44
 *
 * [proleptic Gregorian calendar]: https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar
Thiago Santos's avatar
Thiago Santos committed
45
46
 */

47
48
typedef enum
{
49
50
51
52
53
54
  GST_DATE_TIME_FIELDS_INVALID = 0,
  GST_DATE_TIME_FIELDS_Y,       /* have year                */
  GST_DATE_TIME_FIELDS_YM,      /* have year and month      */
  GST_DATE_TIME_FIELDS_YMD,     /* have year, month and day */
  GST_DATE_TIME_FIELDS_YMD_HM,
  GST_DATE_TIME_FIELDS_YMD_HMS
55
56
      /* Note: if we ever add more granularity here, e.g. for microsecs,
       * the compare function will need updating */
57
} GstDateTimeFields;
58

59
60
struct _GstDateTime
{
61
62
  GstMiniObject mini_object;

63
64
  GDateTime *datetime;

65
  GstDateTimeFields fields;
66
67
};

68
GType _gst_date_time_type = 0;
69
70
71
72
GST_DEFINE_MINI_OBJECT_TYPE (GstDateTime, gst_date_time);

static void gst_date_time_free (GstDateTime * datetime);

73
74
/**
 * gst_date_time_new_from_g_date_time:
75
 * @dt: (transfer full) (nullable): the #GDateTime.
76
77
78
 *
 * Creates a new #GstDateTime from a #GDateTime object.
 *
79
 * Returns: (transfer full) (nullable): a newly created #GstDateTime,
80
 * or %NULL if @dt is %NULL.
81
82
83
 */
GstDateTime *
gst_date_time_new_from_g_date_time (GDateTime * dt)
84
85
86
87
88
89
90
{
  GstDateTime *gst_dt;

  if (!dt)
    return NULL;

  gst_dt = g_slice_new (GstDateTime);
91
92
93
94

  gst_mini_object_init (GST_MINI_OBJECT_CAST (gst_dt), 0, GST_TYPE_DATE_TIME,
      NULL, NULL, (GstMiniObjectFreeFunction) gst_date_time_free);

95
  gst_dt->datetime = dt;
96
  gst_dt->fields = GST_DATE_TIME_FIELDS_YMD_HMS;
97
98
99
  return gst_dt;
}

100
101
102
103
104
105
/**
 * gst_date_time_to_g_date_time:
 * @datetime: GstDateTime.
 *
 * Creates a new #GDateTime from a fully defined #GstDateTime object.
 *
106
 * Returns: (transfer full) (nullable): a newly created #GDateTime, or
107
108
 * %NULL on error or if @datetime does not have a year, month, day, hour,
 * minute and second.
109
110
111
112
113
114
115
116
117
118
119
120
 */
GDateTime *
gst_date_time_to_g_date_time (GstDateTime * datetime)
{
  g_return_val_if_fail (datetime != NULL, NULL);

  if (datetime->fields != GST_DATE_TIME_FIELDS_YMD_HMS)
    return NULL;

  return g_date_time_add (datetime->datetime, 0);
}

121
122
123
124
/**
 * gst_date_time_has_year:
 * @datetime: a #GstDateTime
 *
125
126
 * Returns: %TRUE if @datetime<!-- -->'s year field is set (which should always
 *     be the case), otherwise %FALSE
127
128
129
130
 */
gboolean
gst_date_time_has_year (const GstDateTime * datetime)
{
131
132
133
  g_return_val_if_fail (datetime != NULL, FALSE);

  return (datetime->fields >= GST_DATE_TIME_FIELDS_Y);
134
135
136
137
138
139
}

/**
 * gst_date_time_has_month:
 * @datetime: a #GstDateTime
 *
140
 * Returns: %TRUE if @datetime<!-- -->'s month field is set, otherwise %FALSE
141
142
143
144
 */
gboolean
gst_date_time_has_month (const GstDateTime * datetime)
{
145
146
147
  g_return_val_if_fail (datetime != NULL, FALSE);

  return (datetime->fields >= GST_DATE_TIME_FIELDS_YM);
148
149
150
151
152
153
}

/**
 * gst_date_time_has_day:
 * @datetime: a #GstDateTime
 *
154
 * Returns: %TRUE if @datetime<!-- -->'s day field is set, otherwise %FALSE
155
156
157
158
 */
gboolean
gst_date_time_has_day (const GstDateTime * datetime)
{
159
  g_return_val_if_fail (datetime != NULL, FALSE);
160

161
  return (datetime->fields >= GST_DATE_TIME_FIELDS_YMD);
162
163
164
}

/**
165
 * gst_date_time_has_time:
166
167
 * @datetime: a #GstDateTime
 *
168
169
 * Returns: %TRUE if @datetime<!-- -->'s hour and minute fields are set,
 *     otherwise %FALSE
170
171
 */
gboolean
172
gst_date_time_has_time (const GstDateTime * datetime)
173
{
174
175
176
  g_return_val_if_fail (datetime != NULL, FALSE);

  return (datetime->fields >= GST_DATE_TIME_FIELDS_YMD_HM);
177
178
179
180
181
182
}

/**
 * gst_date_time_has_second:
 * @datetime: a #GstDateTime
 *
183
 * Returns: %TRUE if @datetime<!-- -->'s second field is set, otherwise %FALSE
184
185
186
187
 */
gboolean
gst_date_time_has_second (const GstDateTime * datetime)
{
188
189
190
  g_return_val_if_fail (datetime != NULL, FALSE);

  return (datetime->fields >= GST_DATE_TIME_FIELDS_YMD_HMS);
191
192
}

193
194
195
196
/**
 * gst_date_time_get_year:
 * @datetime: a #GstDateTime
 *
197
 * Returns the year of this #GstDateTime.
198
 * Call gst_date_time_has_year() before, to avoid warnings.
199
200
201
 *
 * Return value: The year of this #GstDateTime
 */
202
203
204
gint
gst_date_time_get_year (const GstDateTime * datetime)
{
205
206
  g_return_val_if_fail (datetime != NULL, 0);

207
208
  return g_date_time_get_year (datetime->datetime);
}
209
210
211
212
213
214
215

/**
 * gst_date_time_get_month:
 * @datetime: a #GstDateTime
 *
 * Returns the month of this #GstDateTime. January is 1, February is 2, etc..
 *
216
 * Return value: The month of this #GstDateTime, or -1 if none is set.
217
 */
218
219
220
gint
gst_date_time_get_month (const GstDateTime * datetime)
{
221
  g_return_val_if_fail (datetime != NULL, 0);
222
223
224

  if (!gst_date_time_has_month (datetime))
    return -1;
225

226
227
  return g_date_time_get_month (datetime->datetime);
}
228
229
230
231
232

/**
 * gst_date_time_get_day:
 * @datetime: a #GstDateTime
 *
233
 * Returns the day of the month of this #GstDateTime.
234
 *
235
 * Return value: The day of this #GstDateTime, or -1 if none is set.
236
 */
237
238
239
gint
gst_date_time_get_day (const GstDateTime * datetime)
{
240
  g_return_val_if_fail (datetime != NULL, 0);
241
242
243

  if (!gst_date_time_has_day (datetime))
    return -1;
244

245
246
  return g_date_time_get_day_of_month (datetime->datetime);
}
247
248
249
250
251
252
253
254

/**
 * gst_date_time_get_hour:
 * @datetime: a #GstDateTime
 *
 * Retrieves the hour of the day represented by @datetime in the gregorian
 * calendar. The return is in the range of 0 to 23.
 *
255
 * Return value: the hour of the day, or -1 if none is set.
256
 */
257
258
259
gint
gst_date_time_get_hour (const GstDateTime * datetime)
{
260
  g_return_val_if_fail (datetime != NULL, 0);
261
262
263

  if (!gst_date_time_has_time (datetime))
    return -1;
264

265
266
  return g_date_time_get_hour (datetime->datetime);
}
267
268
269
270
271
272
273
274

/**
 * gst_date_time_get_minute:
 * @datetime: a #GstDateTime
 *
 * Retrieves the minute of the hour represented by @datetime in the gregorian
 * calendar.
 *
275
 * Return value: the minute of the hour, or -1 if none is set.
276
 */
277
278
279
gint
gst_date_time_get_minute (const GstDateTime * datetime)
{
280
  g_return_val_if_fail (datetime != NULL, 0);
281
282
283

  if (!gst_date_time_has_time (datetime))
    return -1;
284

285
286
  return g_date_time_get_minute (datetime->datetime);
}
287
288
289
290
291
292
293
294

/**
 * gst_date_time_get_second:
 * @datetime: a #GstDateTime
 *
 * Retrieves the second of the minute represented by @datetime in the gregorian
 * calendar.
 *
295
 * Return value: the second represented by @datetime, or -1 if none is set.
296
 */
297
298
299
gint
gst_date_time_get_second (const GstDateTime * datetime)
{
300
  g_return_val_if_fail (datetime != NULL, 0);
301
302
303

  if (!gst_date_time_has_second (datetime))
    return -1;
304

305
306
  return g_date_time_get_second (datetime->datetime);
}
307
308

/**
309
 * gst_date_time_get_microsecond:
310
311
 * @datetime: a #GstDateTime
 *
312
313
 * Retrieves the fractional part of the seconds in microseconds represented by
 * @datetime in the gregorian calendar.
314
 *
315
 * Return value: the microsecond of the second, or -1 if none is set.
316
 */
317
318
319
gint
gst_date_time_get_microsecond (const GstDateTime * datetime)
{
320
  g_return_val_if_fail (datetime != NULL, 0);
321
322
323

  if (!gst_date_time_has_second (datetime))
    return -1;
324

325
326
  return g_date_time_get_microsecond (datetime->datetime);
}
327
328
329
330
331
332
333
334
335
336

/**
 * gst_date_time_get_time_zone_offset:
 * @datetime: a #GstDateTime
 *
 * Retrieves the offset from UTC in hours that the timezone specified
 * by @datetime represents. Timezones ahead (to the east) of UTC have positive
 * values, timezones before (to the west) of UTC have negative values.
 * If @datetime represents UTC time, then the offset is zero.
 *
337
 * Return value: the offset from UTC in hours, or %G_MAXDOUBLE if none is set.
338
 */
339
340
341
gfloat
gst_date_time_get_time_zone_offset (const GstDateTime * datetime)
{
342
  g_return_val_if_fail (datetime != NULL, 0.0);
343
344
345

  if (!gst_date_time_has_time (datetime))
    return G_MAXDOUBLE;
346

347
348
349
  return (g_date_time_get_utc_offset (datetime->datetime) /
      G_USEC_PER_SEC) / 3600.0;
}
350

351
352
353
354
355
356
357
358
359
/**
 * gst_date_time_new_y:
 * @year: the gregorian year
 *
 * Creates a new #GstDateTime using the date and times in the gregorian calendar
 * in the local timezone.
 *
 * @year should be from 1 to 9999.
 *
360
361
 * Return value: (transfer full) (nullable): the newly created #GstDateTime,
 * or %NULL on error.
362
363
364
365
 */
GstDateTime *
gst_date_time_new_y (gint year)
{
366
  return gst_date_time_new (0.0, year, -1, -1, -1, -1, -1);
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
}

/**
 * gst_date_time_new_ym:
 * @year: the gregorian year
 * @month: the gregorian month
 *
 * Creates a new #GstDateTime using the date and times in the gregorian calendar
 * in the local timezone.
 *
 * @year should be from 1 to 9999, @month should be from 1 to 12.
 *
 * If value is -1 then all over value will be ignored. For example
 * if @month == -1, then #GstDateTime will created only for @year.
 *
382
383
 * Return value: (transfer full) (nullable): the newly created #GstDateTime,
 * or %NULL on error.
384
385
386
387
 */
GstDateTime *
gst_date_time_new_ym (gint year, gint month)
{
388
  return gst_date_time_new (0.0, year, month, -1, -1, -1, -1);
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
}

/**
 * gst_date_time_new_ymd:
 * @year: the gregorian year
 * @month: the gregorian month
 * @day: the day of the gregorian month
 *
 * Creates a new #GstDateTime using the date and times in the gregorian calendar
 * in the local timezone.
 *
 * @year should be from 1 to 9999, @month should be from 1 to 12, @day from
 * 1 to 31.
 *
 * If value is -1 then all over value will be ignored. For example
 * if @month == -1, then #GstDateTime will created only for @year. If
 * @day == -1, then #GstDateTime will created for @year and @month and
 * so on.
 *
408
409
 * Return value: (transfer full) (nullable): the newly created #GstDateTime,
 * or %NULL on error.
410
411
412
413
 */
GstDateTime *
gst_date_time_new_ymd (gint year, gint month, gint day)
{
414
  return gst_date_time_new (0.0, year, month, day, -1, -1, -1);
415
416
}

417
/**
418
 * gst_date_time_new_from_unix_epoch_local_time:
419
420
421
422
423
 * @secs: seconds from the Unix epoch
 *
 * Creates a new #GstDateTime using the time since Jan 1, 1970 specified by
 * @secs. The #GstDateTime is in the local timezone.
 *
424
425
 * Return value: (transfer full) (nullable): the newly created #GstDateTime,
 * or %NULL on error.
426
 */
427
428
429
GstDateTime *
gst_date_time_new_from_unix_epoch_local_time (gint64 secs)
{
430
431
432
  GDateTime *datetime;

  datetime = g_date_time_new_from_unix_local (secs);
433
434
435
  if (!datetime)
    return NULL;

436
  return gst_date_time_new_from_g_date_time (datetime);
437
}
438

439
440
441
442
443
444
445
/**
 * gst_date_time_new_from_unix_epoch_utc:
 * @secs: seconds from the Unix epoch
 *
 * Creates a new #GstDateTime using the time since Jan 1, 1970 specified by
 * @secs. The #GstDateTime is in the UTC timezone.
 *
446
447
 * Return value: (transfer full) (nullable): the newly created #GstDateTime,
 * or %NULL on error.
448
 */
449
450
451
GstDateTime *
gst_date_time_new_from_unix_epoch_utc (gint64 secs)
{
452
453
454
455
456
457
458
  GDateTime *datetime;

  datetime = g_date_time_new_from_unix_utc (secs);
  if (!datetime)
    return NULL;

  return gst_date_time_new_from_g_date_time (datetime);
459
460
}

461
462
463
464
465
466
467
/**
 * gst_date_time_new_from_unix_epoch_local_time_usecs:
 * @usecs: microseconds from the Unix epoch
 *
 * Creates a new #GstDateTime using the time since Jan 1, 1970 specified by
 * @usecs. The #GstDateTime is in the local timezone.
 *
468
469
 * Returns: (transfer full) (nullable): a newly created #GstDateTime, or %NULL
 * on error.
470
471
472
473
474
475
476
477
478
479
480
 *
 * Since: 1.18
 */
GstDateTime *
gst_date_time_new_from_unix_epoch_local_time_usecs (gint64 usecs)
{
  GDateTime *dt, *datetime;
  gint64 secs = usecs / G_USEC_PER_SEC;
  gint64 usec_part = usecs % G_USEC_PER_SEC;

  dt = g_date_time_new_from_unix_local (secs);
481
482
  if (!dt)
    return NULL;
483
484
  datetime = g_date_time_add_seconds (dt, (gdouble) usec_part / G_USEC_PER_SEC);
  g_date_time_unref (dt);
485
486
487
  if (!datetime)
    return NULL;

488
489
490
491
492
493
494
495
496
497
  return gst_date_time_new_from_g_date_time (datetime);
}

/**
 * gst_date_time_new_from_unix_epoch_utc_usecs:
 * @usecs: microseconds from the Unix epoch
 *
 * Creates a new #GstDateTime using the time since Jan 1, 1970 specified by
 * @usecs. The #GstDateTime is in UTC.
 *
498
499
 * Returns: (transfer full) (nullable): a newly created #GstDateTime, or %NULL
 * on error.
500
501
502
503
504
505
506
507
508
509
510
 *
 * Since: 1.18
 */
GstDateTime *
gst_date_time_new_from_unix_epoch_utc_usecs (gint64 usecs)
{
  GDateTime *dt, *datetime;
  gint64 secs = usecs / G_USEC_PER_SEC;
  gint64 usec_part = usecs % G_USEC_PER_SEC;

  dt = g_date_time_new_from_unix_utc (secs);
511
512
  if (!dt)
    return NULL;
513
514
  datetime = g_date_time_add_seconds (dt, (gdouble) usec_part / G_USEC_PER_SEC);
  g_date_time_unref (dt);
515
516
517
  if (!datetime)
    return NULL;

518
519
520
  return gst_date_time_new_from_g_date_time (datetime);
}

521
static GstDateTimeFields
522
523
524
525
gst_date_time_check_fields (gint * year, gint * month, gint * day,
    gint * hour, gint * minute, gdouble * seconds)
{
  if (*month == -1) {
526
527
528
    *month = *day = 1;
    *hour = *minute = *seconds = 0;
    return GST_DATE_TIME_FIELDS_Y;
529
  } else if (*day == -1) {
530
531
532
    *day = 1;
    *hour = *minute = *seconds = 0;
    return GST_DATE_TIME_FIELDS_YM;
533
  } else if (*hour == -1) {
534
535
    *hour = *minute = *seconds = 0;
    return GST_DATE_TIME_FIELDS_YMD;
536
  } else if (*seconds == -1) {
537
538
    *seconds = 0;
    return GST_DATE_TIME_FIELDS_YMD_HM;
539
  } else
540
    return GST_DATE_TIME_FIELDS_YMD_HMS;
541
}
542

543
544
545
/**
 * gst_date_time_new_local_time:
 * @year: the gregorian year
546
547
548
549
550
 * @month: the gregorian month, or -1
 * @day: the day of the gregorian month, or -1
 * @hour: the hour of the day, or -1
 * @minute: the minute of the hour, or -1
 * @seconds: the second of the minute, or -1
551
552
553
554
555
 *
 * Creates a new #GstDateTime using the date and times in the gregorian calendar
 * in the local timezone.
 *
 * @year should be from 1 to 9999, @month should be from 1 to 12, @day from
Edward Hervey's avatar
Edward Hervey committed
556
 * 1 to 31, @hour from 0 to 23, @minutes and @seconds from 0 to 59.
557
 *
558
559
560
561
562
563
564
565
566
 * If @month is -1, then the #GstDateTime created will only contain @year,
 * and all other fields will be considered not set.
 *
 * If @day is -1, then the #GstDateTime created will only contain @year and
 * @month and all other fields will be considered not set.
 *
 * If @hour is -1, then the #GstDateTime created will only contain @year and
 * @month and @day, and the time fields will be considered not set. In this
 * case @minute and @seconds should also be -1.
567
 *
568
569
 * Return value: (transfer full) (nullable): the newly created #GstDateTime,
 * or %NULL on error.
570
 */
571
572
573
574
GstDateTime *
gst_date_time_new_local_time (gint year, gint month, gint day, gint hour,
    gint minute, gdouble seconds)
{
575
  GstDateTimeFields fields;
576
  GDateTime *dt;
577
578
  GstDateTime *datetime;

579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
  if (year <= 0 || year > 9999)
    return NULL;

  if ((month <= 0 || month > 12) && month != -1)
    return NULL;

  if ((day <= 0 || day > 31) && day != -1)
    return NULL;

  if ((hour < 0 || hour >= 24) && hour != -1)
    return NULL;

  if ((minute < 0 || minute >= 60) && minute != -1)
    return NULL;

  if ((seconds < 0 || seconds >= 60) && seconds != -1)
    return NULL;
596
597
598
599

  fields = gst_date_time_check_fields (&year, &month, &day,
      &hour, &minute, &seconds);

600
601
602
  dt = g_date_time_new_local (year, month, day, hour, minute, seconds);
  if (dt == NULL)
    return NULL;
603

604
  datetime = gst_date_time_new_from_g_date_time (dt);
605
606
607
  if (datetime == NULL)
    return NULL;

608
609
  datetime->fields = fields;
  return datetime;
610
}
611
612
613
614
615
616

/**
 * gst_date_time_new_now_local_time:
 *
 * Creates a new #GstDateTime representing the current date and time.
 *
617
618
 * Return value: (transfer full) (nullable): the newly created #GstDateTime which should
 *     be freed with gst_date_time_unref(), or %NULL on error.
619
 */
620
621
622
GstDateTime *
gst_date_time_new_now_local_time (void)
{
623
624
625
626
627
628
629
  GDateTime *dt;

  dt = g_date_time_new_now_local ();
  if (!dt)
    return NULL;

  return gst_date_time_new_from_g_date_time (dt);
630
}
631
632
633
634
635
636
637

/**
 * gst_date_time_new_now_utc:
 *
 * Creates a new #GstDateTime that represents the current instant at Universal
 * coordinated time.
 *
638
639
 * Return value: (transfer full) (nullable): the newly created #GstDateTime which should
 *   be freed with gst_date_time_unref(), or %NULL on error.
640
 */
641
642
643
GstDateTime *
gst_date_time_new_now_utc (void)
{
644
645
646
647
648
649
650
  GDateTime *dt;

  dt = g_date_time_new_now_utc ();
  if (!dt)
    return NULL;

  return gst_date_time_new_from_g_date_time (dt);
651
652
653
}

gint
654
__gst_date_time_compare (const GstDateTime * dt1, const GstDateTime * dt2)
655
{
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
  gint64 diff;

  /* we assume here that GST_DATE_TIME_FIELDS_YMD_HMS is the highest
   * resolution, and ignore microsecond differences on purpose for now */
  if (dt1->fields != dt2->fields)
    return GST_VALUE_UNORDERED;

  /* This will round down to nearest second, which is what we want. We're
   * not comparing microseconds on purpose here, since we're not
   * serialising them when doing new_utc_now() + to_string() */
  diff =
      g_date_time_to_unix (dt1->datetime) - g_date_time_to_unix (dt2->datetime);
  if (diff < 0)
    return GST_VALUE_LESS_THAN;
  else if (diff > 0)
    return GST_VALUE_GREATER_THAN;
  else
    return GST_VALUE_EQUAL;
674
675
}

676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
/**
 * gst_date_time_new:
 * @tzoffset: Offset from UTC in hours.
 * @year: the gregorian year
 * @month: the gregorian month
 * @day: the day of the gregorian month
 * @hour: the hour of the day
 * @minute: the minute of the hour
 * @seconds: the second of the minute
 *
 * Creates a new #GstDateTime using the date and times in the gregorian calendar
 * in the supplied timezone.
 *
 * @year should be from 1 to 9999, @month should be from 1 to 12, @day from
 * 1 to 31, @hour from 0 to 23, @minutes and @seconds from 0 to 59.
 *
 * Note that @tzoffset is a float and was chosen so for being able to handle
 * some fractional timezones, while it still keeps the readability of
694
 * representing it in hours for most timezones.
695
 *
696
 * If value is -1 then all over value will be ignored. For example
697
698
 * if @month == -1, then #GstDateTime will be created only for @year. If
 * @day == -1, then #GstDateTime will be created for @year and @month and
699
700
 * so on.
 *
701
702
 * Return value: (transfer full) (nullable): the newly created #GstDateTime,
 * or %NULL on error.
703
 */
704
GstDateTime *
705
706
gst_date_time_new (gfloat tzoffset, gint year, gint month, gint day, gint hour,
    gint minute, gdouble seconds)
707
{
708
  GstDateTimeFields fields;
709
710
711
  gchar buf[6];
  GTimeZone *tz;
  GDateTime *dt;
712
  GstDateTime *datetime;
713
  gint tzhour, tzminute;
714

715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
  if (year <= 0 || year > 9999)
    return NULL;

  if ((month <= 0 || month > 12) && month != -1)
    return NULL;

  if ((day <= 0 || day > 31) && day != -1)
    return NULL;

  if ((hour < 0 || hour >= 24) && hour != -1)
    return NULL;

  if ((minute < 0 || minute >= 60) && minute != -1)
    return NULL;

  if ((seconds < 0 || seconds >= 60) && seconds != -1)
    return NULL;

  if (tzoffset < -12.0 || tzoffset > 12.0)
    return NULL;

  if ((hour < 0 || minute < 0) &&
      (hour != -1 || minute != -1 || seconds != -1 || tzoffset != 0.0))
    return NULL;
739
740
741
742
743
744
745

  tzhour = (gint) ABS (tzoffset);
  tzminute = (gint) ((ABS (tzoffset) - tzhour) * 60);

  g_snprintf (buf, 6, "%c%02d%02d", tzoffset >= 0 ? '+' : '-', tzhour,
      tzminute);

746
747
748
749
750
751
752
#if GLIB_CHECK_VERSION (2, 67, 1)
  /* g_time_zone_new() would always return UTC if the identifier can't be
   * parsed, which is rather suboptimal. */
  tz = g_time_zone_new_identifier (buf);
  if (!tz)
    return NULL;
#else
753
  tz = g_time_zone_new (buf);
754
#endif
755

756
757
758
  fields = gst_date_time_check_fields (&year, &month, &day,
      &hour, &minute, &seconds);

759
  dt = g_date_time_new (tz, year, month, day, hour, minute, seconds);
760
  g_time_zone_unref (tz);
761

762
763
764
  if (!dt)
    return NULL;                /* date failed validation */

765
  datetime = gst_date_time_new_from_g_date_time (dt);
766
767
768
  datetime->fields = fields;

  return datetime;
769
770
}

771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
gchar *
__gst_date_time_serialize (GstDateTime * datetime, gboolean serialize_usecs)
{
  GString *s;
  gfloat gmt_offset;
  guint msecs;

  /* we always have at least the year */
  s = g_string_new (NULL);
  g_string_append_printf (s, "%04u", gst_date_time_get_year (datetime));

  if (datetime->fields == GST_DATE_TIME_FIELDS_Y)
    goto done;

  /* add month */
  g_string_append_printf (s, "-%02u", gst_date_time_get_month (datetime));

  if (datetime->fields == GST_DATE_TIME_FIELDS_YM)
    goto done;

  /* add day of month */
  g_string_append_printf (s, "-%02u", gst_date_time_get_day (datetime));

  if (datetime->fields == GST_DATE_TIME_FIELDS_YMD)
    goto done;

  /* add time */
  g_string_append_printf (s, "T%02u:%02u", gst_date_time_get_hour (datetime),
      gst_date_time_get_minute (datetime));

  if (datetime->fields == GST_DATE_TIME_FIELDS_YMD_HM)
    goto add_timezone;

  /* add seconds */
  g_string_append_printf (s, ":%02u", gst_date_time_get_second (datetime));

  /* add microseconds */
  if (serialize_usecs) {
    msecs = gst_date_time_get_microsecond (datetime);
    if (msecs != 0) {
      g_string_append_printf (s, ".%06u", msecs);
      /* trim trailing 0s */
      while (s->str[s->len - 1] == '0')
        g_string_truncate (s, s->len - 1);
    }
  }

  /* add timezone */

add_timezone:

  gmt_offset = gst_date_time_get_time_zone_offset (datetime);
  if (gmt_offset == 0) {
    g_string_append_c (s, 'Z');
  } else {
    guint tzhour, tzminute;

    tzhour = (guint) ABS (gmt_offset);
    tzminute = (guint) ((ABS (gmt_offset) - tzhour) * 60);

    g_string_append_c (s, (gmt_offset >= 0) ? '+' : '-');
    g_string_append_printf (s, "%02u%02u", tzhour, tzminute);
  }

done:

  return g_string_free (s, FALSE);
}
839
840
841

/**
 * gst_date_time_to_iso8601_string:
842
 * @datetime: a #GstDateTime.
843
844
 *
 * Create a minimal string compatible with ISO-8601. Possible output formats
845
846
 * are (for example): `2012`, `2012-06`, `2012-06-23`, `2012-06-23T23:30Z`,
 * `2012-06-23T23:30+0100`, `2012-06-23T23:30:59Z`, `2012-06-23T23:30:59+0100`
847
 *
848
849
 * Returns: (nullable): a newly allocated string formatted according
 *     to ISO 8601 and only including the datetime fields that are
850
 *     valid, or %NULL in case there was an error.
851
852
853
854
 */
gchar *
gst_date_time_to_iso8601_string (GstDateTime * datetime)
{
855
  g_return_val_if_fail (datetime != NULL, NULL);
856

857
858
859
860
  if (datetime->fields == GST_DATE_TIME_FIELDS_INVALID)
    return NULL;

  return __gst_date_time_serialize (datetime, FALSE);
861
862
863
864
865
866
867
}

/**
 * gst_date_time_new_from_iso8601_string:
 * @string: ISO 8601-formatted datetime string.
 *
 * Tries to parse common variants of ISO-8601 datetime strings into a
868
 * #GstDateTime. Possible input formats are (for example):
869
870
871
872
 * `2012-06-30T22:46:43Z`, `2012`, `2012-06`, `2012-06-30`, `2012-06-30T22:46:43-0430`,
 * `2012-06-30T22:46Z`, `2012-06-30T22:46-0430`, `2012-06-30 22:46`,
 * `2012-06-30 22:46:43`, `2012-06-00`, `2012-00-00`, `2012-00-30`, `22:46:43Z`, `22:46Z`,
 * `22:46:43-0430`, `22:46-0430`, `22:46:30`, `22:46`
873
874
 * If no date is provided, it is assumed to be "today" in the timezone
 * provided (if any), otherwise UTC.
875
 *
876
877
 * Returns: (transfer full) (nullable): a newly created #GstDateTime,
 * or %NULL on error
878
879
880
881
 */
GstDateTime *
gst_date_time_new_from_iso8601_string (const gchar * string)
{
882
  gint year = -1, month = -1, day = -1, hour = -1, minute = -1;
883
  gint gmt_offset_hour = -99, gmt_offset_min = -99;
884
  gdouble second = -1.0;
885
  gfloat tzoffset = 0.0;
886
  guint64 usecs;
887
888
  gint len, ret;

889
890
891
  g_return_val_if_fail (string != NULL, NULL);

  GST_DEBUG ("Parsing '%s' into a datetime", string);
892

893
  len = strlen (string);
894

895
896
897
898
  /* The input string is expected to start either with a year (4 digits) or
   * with an hour (2 digits). Hour must be followed by minute. In any case,
   * the string must be at least 4 characters long and start with 2 digits */
  if (len < 4 || !g_ascii_isdigit (string[0]) || !g_ascii_isdigit (string[1]))
899
    return NULL;
900

901
902
  if (g_ascii_isdigit (string[2]) && g_ascii_isdigit (string[3])) {
    ret = sscanf (string, "%04d-%02d-%02d", &year, &month, &day);
903

904
905
    if (ret == 0)
      return NULL;
906

907
908
909
910
    if (ret == 3 && day <= 0) {
      ret = 2;
      day = -1;
    }
911

912
913
914
915
    if (ret >= 2 && month <= 0) {
      ret = 1;
      month = day = -1;
    }
916

917
    if (ret >= 1 && (year <= 0 || year > 9999 || month > 12 || day > 31))
918
      return NULL;
919

920
921
922
923
    else if (ret >= 1 && len < 16)
      /* YMD is 10 chars. XMD + HM will be 16 chars. if it is less,
       * it make no sense to continue. We will stay with YMD. */
      goto ymd;
924

925
    string += 10;
926
    /* Exit if there is no expected value on this stage */
927
928
    if (!(*string == 'T' || *string == '-' || *string == ' '))
      goto ymd;
929

930
931
932
933
    string += 1;
  }
  /* if hour or minute fails, then we will use only ymd. */
  hour = g_ascii_strtoull (string, (gchar **) & string, 10);
934
935
936
  if (hour > 24 || *string != ':')
    goto ymd;

937
  /* minute */
938
939
940
941
  minute = g_ascii_strtoull (string + 1, (gchar **) & string, 10);
  if (minute > 59)
    goto ymd;

942
  /* second */
943
944
945
  if (*string == ':') {
    second = g_ascii_strtoull (string + 1, (gchar **) & string, 10);
    /* if we fail here, we still can reuse hour and minute. We
946
947
948
     * will still attempt to parse any timezone information */
    if (second > 59) {
      second = -1.0;
949
950
951
952
953
954
955
956
957
958
959
960
    } else {
      /* microseconds */
      if (*string == '.' || *string == ',') {
        const gchar *usec_start = string + 1;
        guint digits;

        usecs = g_ascii_strtoull (string + 1, (gchar **) & string, 10);
        if (usecs != G_MAXUINT64 && string > usec_start) {
          digits = (guint) (string - usec_start);
          second += (gdouble) usecs / pow (10.0, digits);
        }
      }
961
    }
962
963
964
965
966
967
  }

  if (*string == 'Z')
    goto ymd_hms;
  else {
    /* reuse some code from gst-plugins-base/gst-libs/gst/tag/gstxmptag.c */
968
    gint gmt_offset = -1;
969
970
971
972
973
974
975
976
977
978
979
980
981
982
    gchar *plus_pos = NULL;
    gchar *neg_pos = NULL;
    gchar *pos = NULL;

    GST_LOG ("Checking for timezone information");

    /* check if there is timezone info */
    plus_pos = strrchr (string, '+');
    neg_pos = strrchr (string, '-');
    if (plus_pos)
      pos = plus_pos + 1;
    else if (neg_pos)
      pos = neg_pos + 1;

983
    if (pos && strlen (pos) >= 3) {
984
985
986
987
988
989
990
991
992
      gint ret_tz;
      if (pos[2] == ':')
        ret_tz = sscanf (pos, "%d:%d", &gmt_offset_hour, &gmt_offset_min);
      else
        ret_tz = sscanf (pos, "%02d%02d", &gmt_offset_hour, &gmt_offset_min);

      GST_DEBUG ("Parsing timezone: %s", pos);

      if (ret_tz == 2) {
993
994
995
996
        if (neg_pos != NULL && neg_pos + 1 == pos) {
          gmt_offset_hour *= -1;
          gmt_offset_min *= -1;
        }
997
998
999
1000
        gmt_offset = gmt_offset_hour * 60 + gmt_offset_min;

        tzoffset = gmt_offset / 60.0;