2 * convcal : dates conversion utility
4 * Copyright (c) 1999 Luc Maisonobe
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24 * This programs allows you to convert dates between calendar format
25 * and numerical format.
27 * The following command will compile the program :
28 * cc -o convcal convcal.c -lm
39 #define EXIT_SUCCESS 0
40 #define EXIT_FAILURE 1
43 #define REFDATE "-4713-01-01T12:00:00"
45 typedef enum { FMT_iso
,
53 typedef struct { int value
;
59 * set of functions to convert julian calendar elements
60 * with negative years to julian day
62 static int neg_julian_non_leap (int year
)
64 /* one leap year every four years, leap years : -4713, -4709, ..., -5, -1 */
65 return (3 - year
) & 3;
68 static long neg_julian_cal_to_jul(int y
, int m
, int d
)
70 /* day 0 : -4713-01-01
71 * day 1721423 : -1-12-31
73 return (1461L*(y
+ 1L))/4L
74 + (m
*489)/16 - ((m
> 2) ? (neg_julian_non_leap(y
) ? 32L : 31L) : 30L)
79 static int neg_julian_year_estimate(long n
)
81 /* year bounds : 4n - 6887153 <= 1461y <= 4n - 6885693
82 * lower bound reached 31st December of leap years
83 * upper bound reached 1st January of leap years
84 * the lower bound gives a low estimate of the year
86 return (int) ((4L*n
- 6887153L)/1461L);
91 * set of functions to convert julian calendar elements
92 * with positive years to julian day
94 static int pos_julian_non_leap(int year
)
96 /* one leap year every four years, leap years : 4, 8, ..., 1576, 1580 */
100 static long pos_julian_cal_to_jul(int y
, int m
, int d
)
102 /* day 1721424 : 1-01-01
103 * day 2299160 : 1582-10-04
105 return (1461L*(y
-1L))/4L
106 + (m
*489)/16 - ((m
> 2) ? (pos_julian_non_leap(y
) ? 32L : 31L) : 30L)
111 static int pos_julian_year_estimate(long n
)
113 /* year bounds : 4n - 6885692 <= 1461y <= 4n - 6884232
114 * lower bound reached 31st December of leap years
115 * upper bound reached 1st January of leap years
116 * the lower bound gives a low estimate of the year
118 int y
= (int) ((4L*n
- 6885692L)/1461L);
120 /* make sure we stay in the positive model even with our underestimate */
121 return (y
< 1) ? 1 : y
;
127 * set of functions to convert gregorian calendar elements to julian day
129 static int gregorian_non_leap(int year
)
131 /* one leap year every four years, except for multiple of 100 that
132 * are not also multiple of 400 (so 1600, 1896, 1904, and 2000 are
133 * leap years, but 1700, 1800 and 1900 are non leap years
135 return (year
& 3) || ((year
% 100) == 0 && ((year
/100 & 3)));
138 static long gregorian_cal_to_jul(int y
, int m
, int d
)
142 /* day 2299161 : 1582-10-15 */
143 c
= (long) ((y
- 1)/100);
144 return (1461L*(y
- 1))/4 + c
/4 - c
145 + (m
*489)/16 - ((m
> 2) ? (gregorian_non_leap(y
) ? 32L : 31L) : 30L)
150 static int gregorian_year_estimate(long n
)
153 * year bounds : 400n - 688570288 <= 146097y <= 400n - 688423712
154 * lower bound reached on : 1696-12-31, 2096-12-31, 2496-12-31 ...
155 * upper bound reached on : 1904-01-01, 2304-01-01, 2704-01-01 ...
156 * the lower bound gives a low estimate of the year
158 return (int) ((400L*n
- 688570288L)/146097L);
163 * convert calendar elements to Julian day
165 long cal_to_jul(int y
, int m
, int d
)
170 n
= gregorian_cal_to_jul(y
, m
, d
);
173 /* the date belongs to julian calendar */
175 ? neg_julian_cal_to_jul(y
, m
, d
)
176 : pos_julian_cal_to_jul(y
, m
, d
);
185 * convert julian day to calendar elements
187 static void jul_to_some_cal(long n
,
188 int (*some_non_leap
) (int),
189 long (*some_cal_to_jul
) (int, int, int),
190 int (*some_year_estimate
) (long),
191 int *y
, int *m
, int *d
)
193 int non_leap
, day_of_year
, days_until_end_of_year
;
195 /* lower estimation of year */
196 *y
= some_year_estimate(n
);
197 non_leap
= some_non_leap(*y
);
198 days_until_end_of_year
= (int) (some_cal_to_jul(*y
, 12, 31) - n
);
200 while (days_until_end_of_year
< 0) {
201 /* correction of the estimate */
203 non_leap
= some_non_leap(*y
);
204 days_until_end_of_year
+= non_leap
? 365 : 366;
207 day_of_year
= (non_leap
? 365 : 366) - days_until_end_of_year
;
209 /* estimate of the month : one too high only on last days of January */
210 *m
= (16*(day_of_year
+ (non_leap
? 32 : 31))) / 489;
214 - (*m
*489)/16 + ((*m
> 2) ? (non_leap
? 32 : 31) : 30);
216 /* no luck, our estimate is false near end of January */
225 * convert julian day to calendar elements
227 void jul_to_cal(long n
, int *y
, int *m
, int *d
)
230 jul_to_some_cal(n
, neg_julian_non_leap
,
231 neg_julian_cal_to_jul
, neg_julian_year_estimate
,
233 } else if (n
< 2299161L) {
234 jul_to_some_cal(n
, pos_julian_non_leap
,
235 pos_julian_cal_to_jul
, pos_julian_year_estimate
,
238 jul_to_some_cal(n
, gregorian_non_leap
,
239 gregorian_cal_to_jul
, gregorian_year_estimate
,
246 * convert julian day and hourly elements to julian day
248 double jul_and_time_to_jul(long jul
, int hour
, int min
, double sec
)
250 return ((double) jul
)
251 + (((double) (((hour
- 12)*60 + min
)*60)) + sec
)/86400.0;
257 * convert calendar and hourly elements to julian day
259 double cal_and_time_to_jul(int y
, int m
, int d
,
260 int hour
, int min
, double sec
)
262 return jul_and_time_to_jul (cal_to_jul(y
, m
, d
), hour
, min
, sec
);
267 * convert julian day to calendar and hourly elements
268 * rounding_tol allows to say 1999-12-31T23:59:59.501
269 * should be rounded to 2000-01-01T00:00:00.000 assuming
270 * it is set to 0.5 second. It is wise to set it according
271 * to the display accuracy of seconds.
273 void jul_to_cal_and_time(double jday
, double rounding_tol
,
274 int *y
, int *m
, int *d
,
275 int *hour
, int *min
, double *sec
)
279 /* find the time of the day */
280 n
= (long) floor(jday
+ 0.5);
281 *sec
= 24.0*(jday
+ 0.5 - n
);
282 *hour
= (int) floor(*sec
);
283 *sec
= 60.0*(*sec
- *hour
);
284 *min
= (int) floor(*sec
);
285 *sec
= 60.0*(*sec
- *min
);
286 if (*sec
+ rounding_tol
>= 60.0) {
287 /* we should round to next minute */
300 /* now find the date */
301 jul_to_cal(n
, y
, m
, d
);
306 * check the existence of given calendar elements
307 * this includes either number of day in the month
308 * and calendars pecularities (year 0 and October 1582)
310 static int check_date(int century
, int wy
,
311 Int_token y
, Int_token m
, Int_token d
,
314 int y_expand
, y_check
, m_check
, d_check
;
316 /* expands years written with two digits only */
317 if (y
.value
>= 0 && y
.value
< wy
&& y
.digits
<= 2) {
318 y_expand
= century
+ y
.value
;
319 } else if (y
.value
>= wy
&& y
.value
< 100 && y
.digits
<= 2) {
320 y_expand
= century
- 100 + y
.value
;
325 if (m
.digits
> 2 || d
.digits
> 2) {
326 /* this should be the year instead of either the month or the day */
330 *jul
= cal_to_jul(y_expand
, m
.value
, d
.value
);
331 jul_to_cal(*jul
, &y_check
, &m_check
, &d_check
);
332 if (y_expand
!= y_check
|| m
.value
!= m_check
|| d
.value
!= d_check
) {
342 * lexical analyser for float data (knows about fortran exponent
343 * markers, return address of following data)
345 int parse_float(const char* s
, double *value
, const char **after
)
347 int neg_mant
, neg_exp
, digits
, dot_exp
, raw_exp
;
348 const char *after_dot
;
350 /* we skip leading whitespace */
351 while (isspace(*s
)) {
366 while (isdigit(*s
)) {
367 *value
= *value
*10.0 + (*s
++ - '0');
372 while (isdigit(*s
)) {
373 *value
= *value
*10.0 + (*s
++ - '0');
376 dot_exp
= after_dot
- s
;
381 /* there should be at least one digit (either before or after dot) */
385 /* exponent (d and D are fortran exponent markers) */
387 if (*s
== 'e' || *s
== 'E' || *s
== 'd' || *s
== 'D') {
398 while (isdigit(*s
)) {
399 raw_exp
= raw_exp
*10 + (*s
++ - '0');
407 *value
= (neg_mant
? -(*value
) : (*value
)) * pow (10.0, dot_exp
+ raw_exp
);
410 /* the caller wants to know what follows the float number */
420 * lexical analyser for calendar dates
421 * return the number of read elements, or -1 on failure
423 static int parse_calendar_date(const char* s
,
424 Int_token tab
[5], double *sec
)
426 int i
, waiting_separator
, negative
;
429 waiting_separator
= 0;
432 /* loop from year to minute elements : all integers */
435 case '\0': /* end of string */
438 case ' ' : /* repeatable separator */
443 case '/' : case ':' : case '.' : case 'T' : /* non-repeatable separator */
444 if (waiting_separator
) {
445 if ((*s
== 'T') && (i
!= 3)) {
446 /* the T separator is only allowed between date
447 and time (mainly for iso8601) */
452 waiting_separator
= 0;
458 case '-' : /* either separator or minus sign */
460 if (waiting_separator
) {
462 waiting_separator
= 0;
463 } else if ((*s
>= '0') && (*s
<= '9')) {
470 case '0' : case '1' : case '2' : case '3' : case '4' :
471 case '5' : case '6' : case '7' : case '8' : case '9' : /* digit */
472 tab
[i
].value
= ((int) *s
) - '0';
474 while (isdigit(*++s
)) {
475 tab
[i
].value
= tab
[i
].value
*10 + (((int) *s
) - '0');
479 tab
[i
].value
= -tab
[i
].value
;
483 waiting_separator
= 1;
493 while (isspace(*s
)) {
500 if ((*s
== '/') || (*s
== ':') || (*s
== '.') || (*s
== '-')) {
501 /* this was the seconds separator */
504 /* seconds are read in float format */
505 if (parse_float(s
, sec
, &s
) == EXIT_SUCCESS
) {
506 while (isspace(*s
)) {
516 /* something is wrong */
523 * parse a date given either in calendar or numerical format
525 int parse_date(const char* s
, int century
, int wy
, Dates_format preferred
,
526 double *jul
, Dates_format
*recognized
)
530 static Dates_format trials
[] = {FMT_nohint
, FMT_iso
, FMT_european
, FMT_us
};
536 /* first guess : is it a date in calendar format ? */
537 n
= parse_calendar_date(s
, tab
, &sec
);
539 /* we consider hours, minutes and seconds as optional items */
540 case -1 : /* parse error */
544 tab
[3].value
= 0; /* adding hours */
548 tab
[4].value
= 0; /* adding minutes */
552 sec
= 0.0; /* adding seconds */
555 /* we now have a complete date */
557 /* try the user's choice first */
558 trials
[0] = preferred
;
560 for (i
= 0; i
< 4; i
++) {
561 if (trials
[i
] == FMT_iso
) {
566 } else if (trials
[i
] == FMT_european
) {
571 } else if (trials
[i
] == FMT_us
) {
577 /* the user didn't choose a calendar format */
581 if (check_date(century
, wy
, tab
[ky
], tab
[km
], tab
[kd
], &j
)
583 *jul
= jul_and_time_to_jul(j
, tab
[3].value
, tab
[4].value
,
585 *recognized
= trials
[i
];
592 /* probably a julian date (integer if n == 1, real otherwise) */
597 /* second guess : is it a date in numerical format ? */
598 if (parse_float(s
, jul
, &after
) == EXIT_SUCCESS
) {
599 while (isspace(*after
)) {
602 if (*after
== '\0') {
603 if (preferred
== FMT_seconds
) {
604 *recognized
= FMT_seconds
;
607 *recognized
= FMT_days
;
617 int convert_and_write(const char *s
,
618 int century
, int wy
, double reference_date
,
619 Dates_format input_format
, Dates_format output_format
)
621 Dates_format recognized
;
622 int y
, m
, d
, hour
, min
;
626 if (parse_date(s
, century
, wy
, input_format
, &jul
, &recognized
)
631 if (recognized
== FMT_days
|| recognized
== FMT_seconds
) {
632 /* the parsed value is relative to the reference date */
633 jul
+= reference_date
;
636 if (output_format
== FMT_nohint
) {
637 /* choose a format that really convert calendar and numerical */
638 if ((recognized
== FMT_days
) || (recognized
== FMT_seconds
)) {
639 output_format
= FMT_iso
;
641 output_format
= FMT_days
;
645 switch (output_format
) {
647 jul_to_cal_and_time(jul
, 0.0005, &y
, &m
, &d
, &hour
, &min
, &sec
);
648 fprintf(stdout
, "%04d-%02d-%02dT%02d:%02d:%06.3f\n",
649 y
, m
, d
, hour
, min
, sec
);
653 jul_to_cal_and_time(jul
, 0.0005, &y
, &m
, &d
, &hour
, &min
, &sec
);
654 fprintf(stdout
, "%02d/%02d/%04d %02d:%02d:%06.3f\n",
655 d
, m
, y
, hour
, min
, sec
);
659 jul_to_cal_and_time(jul
, 0.0005, &y
, &m
, &d
, &hour
, &min
, &sec
);
660 fprintf(stdout
, "%02d/%02d/%04d %02d:%02d:%06.3f\n",
661 m
, d
, y
, hour
, min
, sec
);
665 fprintf(stdout
, "%17.8f\n", jul
- reference_date
);
669 fprintf(stdout
, "%17.3f\n", 86400.0 * (jul
- reference_date
));
673 fprintf(stderr
, "%s:%d: internal error\n", __FILE__
, __LINE__
);
682 int string_equal(const char *c1
, const char *c2
)
684 return (strlen(c1
) == strlen(c2
)) && (strcmp(c1
, c2
) == 0);
687 int parse_format(const char *s
, Dates_format
*f
)
690 if (string_equal(s
, "iso")) {
692 } else if (string_equal(s
, "european")) {
694 } else if (string_equal(s
, "us")) {
696 } else if (string_equal(s
, "days")) {
698 } else if (string_equal(s
, "seconds")) {
700 } else if (string_equal(s
, "nohint")) {
711 * expand a line buffer
713 static void expand_line_buffer(char **adrBuf
, int *ptrSize
, char **adrPtr
)
718 newsize
= *ptrSize
+ 512;
719 newbuf
= (char *) malloc(newsize
);
721 fprintf(stderr
, "Insufficient memory for line");
726 /* this is the first time through */
731 /* we are expanding an existing line */
732 strncpy(newbuf
, *adrBuf
, *ptrSize
);
734 *adrPtr
+= newbuf
- *adrBuf
;
747 static void usage (FILE *stream
, const char *progname
)
750 "%s reads the dates either on the command line or in the\n", progname
);
752 "standard input if the command line contains no date. The following\n");
754 "date formats are supported (hour, minutes and seconds are always optional):\n");
758 "iso : 1999-12-31T23:59:59.999\n");
760 "european : 31/12/1999 23:59:59.999 or 31/12/99 23:59:59.999\n");
762 "us : 12/31/1999 23:59:59.999 or 12/31/99 23:59:59.999\n");
764 "days : 123456.789\n");
766 "seconds : 123456.789\n");
770 "The formats are tried in the following order : users's choice,\n");
772 "iso, european and us (there is no ambiguity between calendar\n");
774 "formats and numerical formats and therefore no order is specified\n");
776 "for them). The default user's choice (nohint) does nothing so the\n");
778 "following formats of the list are used ; the main use of user's\n");
780 "choice is to put another format before the other ones. The\n");
782 "separators between various fields can be any characters in the set:\n");
784 "\" :/.-T\". One or more spaces act as one separator, other characters\n");
786 "can not be repeated, the T separator is allowed only between date and\n");
788 "time, mainly for iso8601. So the string \"1999-12 31:23-59\" is allowed\n");
790 "(but not recommended). The '-' character is used both as a\n");
792 "separator (it is traditionally used in iso8601 format) and as the\n");
794 "unary minus (for dates in the far past or for numerical\n");
796 "dates). When the year is between 0 and 99 and is written with two\n");
798 "or less digits, it is mapped to the era beginning at wrap year and\n");
800 "ending at wrap year + 99 as follows :\n");
802 " [wy ; 99] -> [ wrap_year ; 100*(1 + wrap_year/100) - 1 ]\n");
804 " [00 ; wy-1] -> [ 100*(1 + wrap_year/100) ; wrap_year + 99]\n");
806 "so for example if the wrap year is set to 1950 (which is the default\n");
808 "value), then the mapping is :\n");
810 " range [00 ; 49] is mapped to [2000 ; 2049]\n");
812 " range [50 ; 99] is mapped to [1950 ; 1999]\n");
814 "this is reasonably Y2K compliant and is consistent with current use.\n");
816 "Specifying year 1 is still possible using more than two digits as\n");
818 "follows : \"0001-03-04\" is unambiguously March the 4th, year 1, even\n");
820 "if the user's choice is us format. However using two digits only is\n");
822 "not recommended (we introduce a 2050 bug here so this feature\n");
824 "should be removed at some point in the future ;-)\n");
828 "Numerical dates (days and seconds formats) can be specified using\n");
830 "integral, real or exponential formats (the 'd' and 'D' exponant\n");
832 "markers from fortran are supported in addition to 'e' and 'E').\n");
834 "They are computed according to a customizable reference date.\n");
836 "The default value is given by the REFDATE constant in the source file.\n");
838 "You can change this value as you want before compiling, and you can\n");
840 "change it at will using the -r command line option. The default\n");
842 "value in the distributed file is \"-4713-01-01T12:00:00\", it is a\n");
844 "classical reference for astronomical events (note that the '-' is\n");
846 "used here both as a unary minus and as a separator).\n");
850 "The program can be used either for Denys's and gregorian\n");
852 "calendars. It does not take into account leap seconds : you can\n");
854 "think it works only in International Atomic Time (TAI) and not in\n");
856 "Coordinated Unified Time (UTC) ... Inexistant dates are detected,\n");
858 "they include year 0, dates between 1582-10-05 and 1582-10-14,\n");
860 "February 29th of non leap years, months below 1 or above 12, ...\n");
864 "The following command line options are supported. Apart from the -h\n");
866 "flag, all of these options can be used several times, each new\n");
868 "value overriding the preceding one.\n");
872 "-i format : set user's choice for input format, supported formats are\n");
874 " iso, european, us, days, seconds and nohint.\n");
876 " At the beginning the input format is nohint, which means\n");
878 " the program try to guess the format by itself, if the\n");
880 " user's choice does not allow to parse the date, other\n");
882 " formats are tried\n");
884 "-o format : force output format, supported formats are\n");
886 " iso, european, us, days, seconds and nohint.\n");
888 " At the beginning, the output format is nohint, which means\n");
890 " the program uses days format for dates read in any\n");
892 " calendar format and uses iso8601 for dates read in\n");
894 " numerical format\n");
896 "-r date : set reference date (the date is read using the current\n");
898 " input format) at the beginning the reference is set\n");
900 " according to the REFDATE constant below.\n");
902 "-w year : set the wrap year to year\n");
904 "-h : prints this help message on stderr and exits successfully\n");
912 int main(int argc
, char *argv
[])
914 double reference_date
;
915 Dates_format input_format
;
916 Dates_format output_format
;
917 Dates_format recognized
;
921 int retval
= EXIT_SUCCESS
;
926 if (parse_date(REFDATE
, century
, wy
, FMT_iso
, &reference_date
, &recognized
)
929 "%s: unable to parse compiled in reference date (%s) !\n",
933 input_format
= FMT_nohint
;
934 output_format
= FMT_nohint
;
936 /* command line parsing */
938 for (i
= 1; i
< argc
; i
= j
) {
941 if (string_equal(argv
[i
], "-i")) {
945 fprintf(stderr
, "%s: missing argument for %s flag\n",
950 if (parse_format(argv
[j
], &input_format
) != EXIT_SUCCESS
) {
951 fprintf(stderr
, "%s: unknown date format \"%s\"\n",
958 } else if (string_equal(argv
[i
], "-o")) {
962 fprintf(stderr
, "%s: missing argument for %s flag\n",
967 if (parse_format(argv
[j
], &output_format
) != EXIT_SUCCESS
) {
968 fprintf(stderr
, "%s: unknown date format \"%s\"\n",
975 } else if (string_equal(argv
[i
], "-r")) {
980 "%s: missing argument for %s flag\n",
985 if (parse_date(argv
[j
], century
, wy
, input_format
,
986 &reference_date
, &recognized
) != EXIT_SUCCESS
) {
988 "%s: unable to parse reference date (%s)\n",
995 } else if (string_equal(argv
[i
], "-w")) {
1000 "%s: missing argument for %s flag\n",
1002 return EXIT_FAILURE
;
1005 century
= 100*(1 + atoi(argv
[j
])/100);
1006 wy
= atoi(argv
[j
]) - (century
- 100);
1010 } else if (string_equal(argv
[i
], "-h")) {
1012 usage(stderr
, argv
[0]);
1016 if (convert_and_write (argv
[i
], century
, wy
, reference_date
,
1017 input_format
, output_format
)
1020 "%s: unable to parse date (%s)\n",
1022 retval
= EXIT_FAILURE
;
1030 if (converted
== 0) {
1031 /* there was no date in the command line : use standard input */
1036 expand_line_buffer (&line
, &size
, NULL
);
1039 /* input lines reading loop */
1040 char *cursor
= line
+ 1;
1045 while (reading
!= 0 && *(cursor
- 1) != '\n') {
1046 /* trying to read until end of line */
1048 if (size
- (cursor
- line
) < 2) {
1049 /* there is not enough room left */
1050 expand_line_buffer(&line
, &size
, &cursor
);
1053 if (fgets(cursor
, size
- (cursor
- line
), stdin
) == NULL
) {
1054 if (cursor
== line
+ 1) {
1058 /* something went wrong */
1060 "%s: read error on line %d: %s\n",
1061 argv
[0], num_line
, line
+ 1);
1062 retval
= EXIT_FAILURE
;
1065 /* something has been successfully read */
1066 cursor
+= strlen(cursor
);
1069 *(cursor
- 1) = '\0';
1072 /* converting the date */
1073 if (convert_and_write (line
+ 1, century
, wy
, reference_date
,
1074 input_format
, output_format
)
1077 "%s: unable to parse date (%s)\n",
1079 retval
= EXIT_FAILURE
;