PostgreSQL Source Code  git master
 All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros
datetime.c
Go to the documentation of this file.
1 /* src/interfaces/ecpg/pgtypeslib/datetime.c */
2 
3 #include "postgres_fe.h"
4 
5 #include <time.h>
6 #include <ctype.h>
7 #include <float.h>
8 #include <limits.h>
9 
10 #include "extern.h"
11 #include "dt.h"
12 #include "pgtypes_error.h"
13 #include "pgtypes_date.h"
14 
15 date *
17 {
18  date *result;
19 
20  result = (date *) pgtypes_alloc(sizeof(date));
21  /* result can be NULL if we run out of memory */
22  return result;
23 }
24 
25 void
27 {
28  free(d);
29 }
30 
31 date
33 {
34  date dDate;
35 
36  dDate = 0; /* suppress compiler warning */
37 
38  if (!TIMESTAMP_NOT_FINITE(dt))
39  {
40 #ifdef HAVE_INT64_TIMESTAMP
41  /* Microseconds to days */
42  dDate = (dt / USECS_PER_DAY);
43 #else
44  /* Seconds to days */
45  dDate = (dt / (double) SECS_PER_DAY);
46 #endif
47  }
48 
49  return dDate;
50 }
51 
52 date
53 PGTYPESdate_from_asc(char *str, char **endptr)
54 {
55  date dDate;
56  fsec_t fsec;
57  struct tm tt,
58  *tm = &tt;
59  int dtype;
60  int nf;
61  char *field[MAXDATEFIELDS];
62  int ftype[MAXDATEFIELDS];
63  char lowstr[MAXDATELEN + MAXDATEFIELDS];
64  char *realptr;
65  char **ptr = (endptr != NULL) ? endptr : &realptr;
66 
67  bool EuroDates = FALSE;
68 
69  errno = 0;
70  if (strlen(str) > MAXDATELEN)
71  {
72  errno = PGTYPES_DATE_BAD_DATE;
73  return INT_MIN;
74  }
75 
76  if (ParseDateTime(str, lowstr, field, ftype, &nf, ptr) != 0 ||
77  DecodeDateTime(field, ftype, nf, &dtype, tm, &fsec, EuroDates) != 0)
78  {
79  errno = PGTYPES_DATE_BAD_DATE;
80  return INT_MIN;
81  }
82 
83  switch (dtype)
84  {
85  case DTK_DATE:
86  break;
87 
88  case DTK_EPOCH:
89  if (GetEpochTime(tm) < 0)
90  {
91  errno = PGTYPES_DATE_BAD_DATE;
92  return INT_MIN;
93  }
94  break;
95 
96  default:
97  errno = PGTYPES_DATE_BAD_DATE;
98  return INT_MIN;
99  }
100 
101  dDate = (date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - date2j(2000, 1, 1));
102 
103  return dDate;
104 }
105 
106 char *
108 {
109  struct tm tt,
110  *tm = &tt;
111  char buf[MAXDATELEN + 1];
112  int DateStyle = 1;
113  bool EuroDates = FALSE;
114 
115  j2date(dDate + date2j(2000, 1, 1), &(tm->tm_year), &(tm->tm_mon), &(tm->tm_mday));
116  EncodeDateOnly(tm, DateStyle, buf, EuroDates);
117  return pgtypes_strdup(buf);
118 }
119 
120 void
122 {
123  int y,
124  m,
125  d;
126 
127  j2date((int) (jd + date2j(2000, 1, 1)), &y, &m, &d);
128  mdy[0] = m;
129  mdy[1] = d;
130  mdy[2] = y;
131 }
132 
133 void
134 PGTYPESdate_mdyjul(int *mdy, date * jdate)
135 {
136  /* month is mdy[0] */
137  /* day is mdy[1] */
138  /* year is mdy[2] */
139 
140  *jdate = (date) (date2j(mdy[2], mdy[0], mdy[1]) - date2j(2000, 1, 1));
141 }
142 
143 int
145 {
146  /*
147  * Sunday: 0 Monday: 1 Tuesday: 2 Wednesday: 3 Thursday: 4
148  * Friday: 5 Saturday: 6
149  */
150  return (int) (dDate + date2j(2000, 1, 1) + 1) % 7;
151 }
152 
153 void
155 {
156  struct tm ts;
157 
158  GetCurrentDateTime(&ts);
159  if (errno == 0)
160  *d = date2j(ts.tm_year, ts.tm_mon, ts.tm_mday) - date2j(2000, 1, 1);
161  return;
162 }
163 
164 #define PGTYPES_DATE_NUM_MAX_DIGITS 20 /* should suffice for most
165  * years... */
166 
167 #define PGTYPES_FMTDATE_DAY_DIGITS_LZ 1 /* LZ means "leading zeroes" */
168 #define PGTYPES_FMTDATE_DOW_LITERAL_SHORT 2
169 #define PGTYPES_FMTDATE_MONTH_DIGITS_LZ 3
170 #define PGTYPES_FMTDATE_MONTH_LITERAL_SHORT 4
171 #define PGTYPES_FMTDATE_YEAR_DIGITS_SHORT 5
172 #define PGTYPES_FMTDATE_YEAR_DIGITS_LONG 6
173 
174 int
175 PGTYPESdate_fmt_asc(date dDate, const char *fmtstring, char *outbuf)
176 {
177  static struct
178  {
179  char *format;
180  int component;
181  } mapping[] =
182  {
183  /*
184  * format items have to be sorted according to their length, since the
185  * first pattern that matches gets replaced by its value
186  */
187  {
189  },
190  {
192  },
193  {
195  },
196  {
198  },
199  {
201  },
202  {
204  },
205  {
206  NULL, 0
207  }
208  };
209 
210  union un_fmt_comb replace_val;
211  int replace_type;
212 
213  int i;
214  int dow;
215  char *start_pattern;
216  struct tm tm;
217 
218  /* copy the string over */
219  strcpy(outbuf, fmtstring);
220 
221  /* get the date */
222  j2date(dDate + date2j(2000, 1, 1), &(tm.tm_year), &(tm.tm_mon), &(tm.tm_mday));
223  dow = PGTYPESdate_dayofweek(dDate);
224 
225  for (i = 0; mapping[i].format != NULL; i++)
226  {
227  while ((start_pattern = strstr(outbuf, mapping[i].format)) != NULL)
228  {
229  switch (mapping[i].component)
230  {
232  replace_val.str_val = pgtypes_date_weekdays_short[dow];
233  replace_type = PGTYPES_TYPE_STRING_CONSTANT;
234  break;
236  replace_val.uint_val = tm.tm_mday;
237  replace_type = PGTYPES_TYPE_UINT_2_LZ;
238  break;
240  replace_val.str_val = months[tm.tm_mon - 1];
241  replace_type = PGTYPES_TYPE_STRING_CONSTANT;
242  break;
244  replace_val.uint_val = tm.tm_mon;
245  replace_type = PGTYPES_TYPE_UINT_2_LZ;
246  break;
248  replace_val.uint_val = tm.tm_year;
249  replace_type = PGTYPES_TYPE_UINT_4_LZ;
250  break;
252  replace_val.uint_val = tm.tm_year % 100;
253  replace_type = PGTYPES_TYPE_UINT_2_LZ;
254  break;
255  default:
256 
257  /*
258  * should not happen, set something anyway
259  */
260  replace_val.str_val = " ";
261  replace_type = PGTYPES_TYPE_STRING_CONSTANT;
262  }
263  switch (replace_type)
264  {
267  memcpy(start_pattern, replace_val.str_val,
268  strlen(replace_val.str_val));
269  if (replace_type == PGTYPES_TYPE_STRING_MALLOCED)
270  free(replace_val.str_val);
271  break;
272  case PGTYPES_TYPE_UINT:
273  {
275 
276  if (!t)
277  return -1;
279  "%u", replace_val.uint_val);
280  memcpy(start_pattern, t, strlen(t));
281  free(t);
282  }
283  break;
285  {
287 
288  if (!t)
289  return -1;
291  "%02u", replace_val.uint_val);
292  memcpy(start_pattern, t, strlen(t));
293  free(t);
294  }
295  break;
297  {
299 
300  if (!t)
301  return -1;
303  "%04u", replace_val.uint_val);
304  memcpy(start_pattern, t, strlen(t));
305  free(t);
306  }
307  break;
308  default:
309 
310  /*
311  * doesn't happen (we set replace_type to
312  * PGTYPES_TYPE_STRING_CONSTANT in case of an error above)
313  */
314  break;
315  }
316  }
317  }
318  return 0;
319 }
320 
321 
322 /*
323  * PGTYPESdate_defmt_asc
324  *
325  * function works as follows:
326  * - first we analyze the parameters
327  * - if this is a special case with no delimiters, add delimiters
328  * - find the tokens. First we look for numerical values. If we have found
329  * less than 3 tokens, we check for the months' names and thereafter for
330  * the abbreviations of the months' names.
331  * - then we see which parameter should be the date, the month and the
332  * year and from these values we calculate the date
333  */
334 
335 #define PGTYPES_DATE_MONTH_MAXLENGTH 20 /* probably even less :-) */
336 int
337 PGTYPESdate_defmt_asc(date * d, const char *fmt, char *str)
338 {
339  /*
340  * token[2] = { 4,6 } means that token 2 starts at position 4 and ends at
341  * (including) position 6
342  */
343  int token[3][2];
344  int token_values[3] = {-1, -1, -1};
345  char *fmt_token_order;
346  char *fmt_ystart,
347  *fmt_mstart,
348  *fmt_dstart;
349  unsigned int i;
350  int reading_digit;
351  int token_count;
352  char *str_copy;
353  struct tm tm;
354 
355  tm.tm_year = tm.tm_mon = tm.tm_mday = 0; /* keep compiler quiet */
356 
357  if (!d || !str || !fmt)
358  {
359  errno = PGTYPES_DATE_ERR_EARGS;
360  return -1;
361  }
362 
363  /* analyze the fmt string */
364  fmt_ystart = strstr(fmt, "yy");
365  fmt_mstart = strstr(fmt, "mm");
366  fmt_dstart = strstr(fmt, "dd");
367 
368  if (!fmt_ystart || !fmt_mstart || !fmt_dstart)
369  {
370  errno = PGTYPES_DATE_ERR_EARGS;
371  return -1;
372  }
373 
374  if (fmt_ystart < fmt_mstart)
375  {
376  /* y m */
377  if (fmt_dstart < fmt_ystart)
378  {
379  /* d y m */
380  fmt_token_order = "dym";
381  }
382  else if (fmt_dstart > fmt_mstart)
383  {
384  /* y m d */
385  fmt_token_order = "ymd";
386  }
387  else
388  {
389  /* y d m */
390  fmt_token_order = "ydm";
391  }
392  }
393  else
394  {
395  /* fmt_ystart > fmt_mstart */
396  /* m y */
397  if (fmt_dstart < fmt_mstart)
398  {
399  /* d m y */
400  fmt_token_order = "dmy";
401  }
402  else if (fmt_dstart > fmt_ystart)
403  {
404  /* m y d */
405  fmt_token_order = "myd";
406  }
407  else
408  {
409  /* m d y */
410  fmt_token_order = "mdy";
411  }
412  }
413 
414  /*
415  * handle the special cases where there is no delimiter between the
416  * digits. If we see this:
417  *
418  * only digits, 6 or 8 bytes then it might be ddmmyy and ddmmyyyy (or
419  * similar)
420  *
421  * we reduce it to a string with delimiters and continue processing
422  */
423 
424  /* check if we have only digits */
425  reading_digit = 1;
426  for (i = 0; str[i]; i++)
427  {
428  if (!isdigit((unsigned char) str[i]))
429  {
430  reading_digit = 0;
431  break;
432  }
433  }
434  if (reading_digit)
435  {
436  int frag_length[3];
437  int target_pos;
438 
439  i = strlen(str);
440  if (i != 8 && i != 6)
441  {
443  return -1;
444  }
445  /* okay, this really is the special case */
446 
447  /*
448  * as long as the string, one additional byte for the terminator and 2
449  * for the delimiters between the 3 fiedls
450  */
451  str_copy = pgtypes_alloc(strlen(str) + 1 + 2);
452  if (!str_copy)
453  return -1;
454 
455  /* determine length of the fragments */
456  if (i == 6)
457  {
458  frag_length[0] = 2;
459  frag_length[1] = 2;
460  frag_length[2] = 2;
461  }
462  else
463  {
464  if (fmt_token_order[0] == 'y')
465  {
466  frag_length[0] = 4;
467  frag_length[1] = 2;
468  frag_length[2] = 2;
469  }
470  else if (fmt_token_order[1] == 'y')
471  {
472  frag_length[0] = 2;
473  frag_length[1] = 4;
474  frag_length[2] = 2;
475  }
476  else
477  {
478  frag_length[0] = 2;
479  frag_length[1] = 2;
480  frag_length[2] = 4;
481  }
482  }
483  target_pos = 0;
484 
485  /*
486  * XXX: Here we could calculate the positions of the tokens and save
487  * the for loop down there where we again check with isdigit() for
488  * digits.
489  */
490  for (i = 0; i < 3; i++)
491  {
492  int start_pos = 0;
493 
494  if (i >= 1)
495  start_pos += frag_length[0];
496  if (i == 2)
497  start_pos += frag_length[1];
498 
499  strncpy(str_copy + target_pos, str + start_pos,
500  frag_length[i]);
501  target_pos += frag_length[i];
502  if (i != 2)
503  {
504  str_copy[target_pos] = ' ';
505  target_pos++;
506  }
507  }
508  str_copy[target_pos] = '\0';
509  }
510  else
511  {
512  str_copy = pgtypes_strdup(str);
513  if (!str_copy)
514  return -1;
515 
516  /* convert the whole string to lower case */
517  for (i = 0; str_copy[i]; i++)
518  str_copy[i] = (char) pg_tolower((unsigned char) str_copy[i]);
519  }
520 
521  /* look for numerical tokens */
522  reading_digit = 0;
523  token_count = 0;
524  for (i = 0; i < strlen(str_copy); i++)
525  {
526  if (!isdigit((unsigned char) str_copy[i]) && reading_digit)
527  {
528  /* the token is finished */
529  token[token_count][1] = i - 1;
530  reading_digit = 0;
531  token_count++;
532  }
533  else if (isdigit((unsigned char) str_copy[i]) && !reading_digit)
534  {
535  /* we have found a token */
536  token[token_count][0] = i;
537  reading_digit = 1;
538  }
539  }
540 
541  /*
542  * we're at the end of the input string, but maybe we are still reading a
543  * number...
544  */
545  if (reading_digit)
546  {
547  token[token_count][1] = i - 1;
548  token_count++;
549  }
550 
551 
552  if (token_count < 2)
553  {
554  /*
555  * not all tokens found, no way to find 2 missing tokens with string
556  * matches
557  */
558  free(str_copy);
560  return -1;
561  }
562 
563  if (token_count != 3)
564  {
565  /*
566  * not all tokens found but we may find another one with string
567  * matches by testing for the months names and months abbreviations
568  */
569  char *month_lower_tmp = pgtypes_alloc(PGTYPES_DATE_MONTH_MAXLENGTH);
570  char *start_pos;
571  int j;
572  int offset;
573  int found = 0;
574  char **list;
575 
576  if (!month_lower_tmp)
577  {
578  /* free variables we alloc'ed before */
579  free(str_copy);
580  return -1;
581  }
582  list = pgtypes_date_months;
583  for (i = 0; list[i]; i++)
584  {
585  for (j = 0; j < PGTYPES_DATE_MONTH_MAXLENGTH; j++)
586  {
587  month_lower_tmp[j] = (char) pg_tolower((unsigned char) list[i][j]);
588  if (!month_lower_tmp[j])
589  {
590  /* properly terminated */
591  break;
592  }
593  }
594  if ((start_pos = strstr(str_copy, month_lower_tmp)))
595  {
596  offset = start_pos - str_copy;
597 
598  /*
599  * sort the new token into the numeric tokens, shift them if
600  * necessary
601  */
602  if (offset < token[0][0])
603  {
604  token[2][0] = token[1][0];
605  token[2][1] = token[1][1];
606  token[1][0] = token[0][0];
607  token[1][1] = token[0][1];
608  token_count = 0;
609  }
610  else if (offset < token[1][0])
611  {
612  token[2][0] = token[1][0];
613  token[2][1] = token[1][1];
614  token_count = 1;
615  }
616  else
617  token_count = 2;
618  token[token_count][0] = offset;
619  token[token_count][1] = offset + strlen(month_lower_tmp) - 1;
620 
621  /*
622  * the value is the index of the month in the array of months
623  * + 1 (January is month 0)
624  */
625  token_values[token_count] = i + 1;
626  found = 1;
627  break;
628  }
629 
630  /*
631  * evil[tm] hack: if we read the pgtypes_date_months and haven't
632  * found a match, reset list to point to pgtypes_date_months_short
633  * and reset the counter variable i
634  */
635  if (list == pgtypes_date_months)
636  {
637  if (list[i + 1] == NULL)
638  {
639  list = months;
640  i = -1;
641  }
642  }
643  }
644  if (!found)
645  {
646  free(month_lower_tmp);
647  free(str_copy);
648  errno = PGTYPES_DATE_ERR_ENOTDMY;
649  return -1;
650  }
651 
652  /*
653  * here we found a month. token[token_count] and
654  * token_values[token_count] reflect the month's details.
655  *
656  * only the month can be specified with a literal. Here we can do a
657  * quick check if the month is at the right position according to the
658  * format string because we can check if the token that we expect to
659  * be the month is at the position of the only token that already has
660  * a value. If we wouldn't check here we could say "December 4 1990"
661  * with a fmt string of "dd mm yy" for 12 April 1990.
662  */
663  if (fmt_token_order[token_count] != 'm')
664  {
665  /* deal with the error later on */
666  token_values[token_count] = -1;
667  }
668  free(month_lower_tmp);
669  }
670 
671  /* terminate the tokens with ASCII-0 and get their values */
672  for (i = 0; i < 3; i++)
673  {
674  *(str_copy + token[i][1] + 1) = '\0';
675  /* A month already has a value set, check for token_value == -1 */
676  if (token_values[i] == -1)
677  {
678  errno = 0;
679  token_values[i] = strtol(str_copy + token[i][0], (char **) NULL, 10);
680  /* strtol sets errno in case of an error */
681  if (errno)
682  token_values[i] = -1;
683  }
684  if (fmt_token_order[i] == 'd')
685  tm.tm_mday = token_values[i];
686  else if (fmt_token_order[i] == 'm')
687  tm.tm_mon = token_values[i];
688  else if (fmt_token_order[i] == 'y')
689  tm.tm_year = token_values[i];
690  }
691  free(str_copy);
692 
693  if (tm.tm_mday < 1 || tm.tm_mday > 31)
694  {
695  errno = PGTYPES_DATE_BAD_DAY;
696  return -1;
697  }
698 
699  if (tm.tm_mon < 1 || tm.tm_mon > MONTHS_PER_YEAR)
700  {
701  errno = PGTYPES_DATE_BAD_MONTH;
702  return -1;
703  }
704 
705  if (tm.tm_mday == 31 && (tm.tm_mon == 4 || tm.tm_mon == 6 || tm.tm_mon == 9 || tm.tm_mon == 11))
706  {
707  errno = PGTYPES_DATE_BAD_DAY;
708  return -1;
709  }
710 
711  if (tm.tm_mon == 2 && tm.tm_mday > 29)
712  {
713  errno = PGTYPES_DATE_BAD_DAY;
714  return -1;
715  }
716 
717  *d = date2j(tm.tm_year, tm.tm_mon, tm.tm_mday) - date2j(2000, 1, 1);
718 
719  return 0;
720 }
#define MAXDATELEN
Definition: datetime.h:203
void EncodeDateOnly(struct pg_tm *tm, int style, char *str)
Definition: datetime.c:3990
date PGTYPESdate_from_timestamp(timestamp dt)
Definition: datetime.c:32
#define PGTYPES_DATE_BAD_DATE
Definition: pgtypes_error.h:8
void GetCurrentDateTime(struct pg_tm *tm)
Definition: datetime.c:375
#define PGTYPES_TYPE_STRING_MALLOCED
Definition: extern.h:11
#define PGTYPES_DATE_BAD_DAY
Definition: pgtypes_error.h:12
#define PGTYPES_DATE_ERR_EARGS
Definition: pgtypes_error.h:9
double fsec_t
Definition: timestamp.h:53
#define PGTYPES_FMTDATE_DAY_DIGITS_LZ
Definition: datetime.c:167
#define PGTYPES_DATE_ERR_ENOTDMY
Definition: pgtypes_error.h:11
int PGTYPESdate_fmt_asc(date dDate, const char *fmtstring, char *outbuf)
Definition: datetime.c:175
void PGTYPESdate_today(date *d)
Definition: datetime.c:154
date PGTYPESdate_from_asc(char *str, char **endptr)
Definition: datetime.c:53
char * pgtypes_date_weekdays_short[]
Definition: dt_common.c:501
unsigned char pg_tolower(unsigned char ch)
Definition: pgstrcasecmp.c:122
int snprintf(char *str, size_t count, const char *fmt,...) pg_attribute_printf(3
long date
Definition: pgtypes_date.h:8
#define PGTYPES_DATE_BAD_MONTH
Definition: pgtypes_error.h:13
int PGTYPESdate_defmt_asc(date *d, const char *fmt, char *str)
Definition: datetime.c:337
#define PGTYPES_DATE_NUM_MAX_DIGITS
Definition: datetime.c:164
static struct pg_tm tm
Definition: localtime.c:103
#define MONTHS_PER_YEAR
Definition: timestamp.h:81
char * pgtypes_strdup(const char *str)
Definition: common.c:19
#define TIMESTAMP_NOT_FINITE(j)
Definition: timestamp.h:144
void PGTYPESdate_julmdy(date jd, int *mdy)
Definition: datetime.c:121
void PGTYPESdate_mdyjul(int *mdy, date *jdate)
Definition: datetime.c:134
int DecodeDateTime(char **field, int *ftype, int nf, int *dtype, struct pg_tm *tm, fsec_t *fsec, int *tzp)
Definition: datetime.c:847
const char *const months[]
Definition: datetime.c:71
#define SECS_PER_DAY
Definition: timestamp.h:98
#define FALSE
Definition: c.h:218
#define PGTYPES_DATE_ERR_ENOSHORTDATE
Definition: pgtypes_error.h:10
char * pgtypes_alloc(long size)
Definition: common.c:9
static char * buf
Definition: pg_test_fsync.c:65
#define PGTYPES_FMTDATE_YEAR_DIGITS_LONG
Definition: datetime.c:172
#define PGTYPES_DATE_MONTH_MAXLENGTH
Definition: datetime.c:335
date * PGTYPESdate_new(void)
Definition: datetime.c:16
#define USECS_PER_DAY
Definition: timestamp.h:103
void j2date(int jd, int *year, int *month, int *day)
Definition: datetime.c:322
#define PGTYPES_TYPE_UINT_4_LZ
Definition: extern.h:22
#define PGTYPES_FMTDATE_YEAR_DIGITS_SHORT
Definition: datetime.c:171
int date2j(int y, int m, int d)
Definition: datetime.c:297
char * pgtypes_date_months[]
Definition: dt_common.c:503
#define free(a)
Definition: header.h:60
#define NULL
Definition: c.h:226
char * str_val
Definition: extern.h:29
#define PGTYPES_FMTDATE_MONTH_LITERAL_SHORT
Definition: datetime.c:170
#define MAXDATEFIELDS
Definition: datetime.h:205
#define PGTYPES_TYPE_UINT_2_LZ
Definition: extern.h:17
#define PGTYPES_TYPE_STRING_CONSTANT
Definition: extern.h:12
int DateStyle
Definition: globals.c:106
void PGTYPESdate_free(date *d)
Definition: datetime.c:26
double timestamp
#define DTK_EPOCH
Definition: datetime.h:155
tuple list
Definition: sort-test.py:11
int tm_year
Definition: pgtime.h:32
int i
void GetEpochTime(struct pg_tm *tm)
Definition: timestamp.c:2255
int PGTYPESdate_dayofweek(date dDate)
Definition: datetime.c:144
#define PGTYPES_FMTDATE_DOW_LITERAL_SHORT
Definition: datetime.c:168
static char format
Definition: pg_basebackup.c:83
char * PGTYPESdate_to_asc(date dDate)
Definition: datetime.c:107
int ParseDateTime(const char *timestr, char *workbuf, size_t buflen, char **field, int *ftype, int maxfields, int *numfields)
Definition: datetime.c:626
#define PGTYPES_TYPE_UINT
Definition: extern.h:16
#define PGTYPES_FMTDATE_MONTH_DIGITS_LZ
Definition: datetime.c:169
unsigned int uint_val
Definition: extern.h:30
#define DTK_DATE
Definition: datetime.h:145