xref: /php-src/ext/calendar/calendar.c (revision 41996e8d)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Authors: Shane Caraveo             <shane@caraveo.com>               |
14    |          Colin Viebrock            <colin@easydns.com>               |
15    |          Hartmut Holzgraefe        <hholzgra@php.net>                |
16    |          Wez Furlong               <wez@thebrainroom.com>            |
17    +----------------------------------------------------------------------+
18  */
19 
20 #ifdef HAVE_CONFIG_H
21 #include <config.h>
22 #endif
23 
24 #include "php.h"
25 #include "ext/standard/info.h"
26 #include "php_calendar.h"
27 #include "sdncal.h"
28 
29 #include <stdio.h>
30 
31 #ifdef PHP_WIN32
32 /* This conflicts with a define in winnls.h, but that header is needed
33    to have GetACP(). */
34 #undef CAL_GREGORIAN
35 #endif
36 
37 /* this order must match the conversion table below */
38 enum cal_name_type_t {
39 	CAL_GREGORIAN = 0,
40 	CAL_JULIAN,
41 	CAL_JEWISH,
42 	CAL_FRENCH,
43 	CAL_NUM_CALS
44 };
45 
46 typedef zend_long (*cal_to_jd_func_t) (int month, int day, int year);
47 typedef void (*cal_from_jd_func_t) (zend_long jd, int *year, int *month, int *day);
48 typedef char *(*cal_as_string_func_t) (int year, int month, int day);
49 
50 struct cal_entry_t {
51 	const char *name;
52 	const char *symbol;
53 	cal_to_jd_func_t to_jd;
54 	cal_from_jd_func_t from_jd;
55 	int num_months;
56 	int max_days_in_month;
57 	const char * const * month_name_short;
58 	const char * const * month_name_long;
59 };
60 
61 static const struct cal_entry_t cal_conversion_table[CAL_NUM_CALS] = {
62 	{"Gregorian", "CAL_GREGORIAN", GregorianToSdn, SdnToGregorian, 12, 31,
63 	 MonthNameShort, MonthNameLong},
64 	{"Julian", "CAL_JULIAN", JulianToSdn, SdnToJulian, 12, 31,
65 	 MonthNameShort, MonthNameLong},
66 	{"Jewish", "CAL_JEWISH", JewishToSdn, SdnToJewish, 13, 30,
67 	 JewishMonthNameLeap, JewishMonthNameLeap},
68 	{"French", "CAL_FRENCH", FrenchToSdn, SdnToFrench, 13, 30,
69 	 FrenchMonthName, FrenchMonthName}
70 };
71 
72 #define JEWISH_MONTH_NAME(year) 	((monthsPerYear[((year)-1) % 19] == 13)?JewishMonthNameLeap:JewishMonthName)
73 #define JEWISH_HEB_MONTH_NAME(year) ((monthsPerYear[((year)-1) % 19] == 13)?JewishMonthHebNameLeap:JewishMonthHebName)
74 
75 /* For jddayofweek */
76 enum { CAL_DOW_DAYNO, CAL_DOW_LONG, CAL_DOW_SHORT };
77 
78 /* For jdmonthname */
79 enum { CAL_MONTH_GREGORIAN_SHORT, CAL_MONTH_GREGORIAN_LONG,
80 	CAL_MONTH_JULIAN_SHORT, CAL_MONTH_JULIAN_LONG, CAL_MONTH_JEWISH,
81 	CAL_MONTH_FRENCH
82 };
83 
84 /* For heb_number_to_chars escape sequences of אבגדהוזחטיכלמנסעפצקרשת
85    ISO-8859-8 Hebrew alphabet */
86 static const char alef_bet[25] = "0\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEB\xEC\xEE\xF0\xF1\xF2\xF4\xF6\xF7\xF8\xF9\xFA";
87 
88 #define CAL_JEWISH_ADD_ALAFIM_GERESH 0x2
89 #define CAL_JEWISH_ADD_ALAFIM 0x4
90 #define CAL_JEWISH_ADD_GERESHAYIM 0x8
91 
92 #include "calendar_arginfo.h"
93 
94 zend_module_entry calendar_module_entry = {
95 	STANDARD_MODULE_HEADER,
96 	"calendar",
97 	ext_functions,
98 	PHP_MINIT(calendar),
99 	NULL,
100 	NULL,
101 	NULL,
102 	PHP_MINFO(calendar),
103 	PHP_CALENDAR_VERSION,
104 	STANDARD_MODULE_PROPERTIES,
105 };
106 
107 #ifdef COMPILE_DL_CALENDAR
108 ZEND_GET_MODULE(calendar)
109 #endif
110 
PHP_MINIT_FUNCTION(calendar)111 PHP_MINIT_FUNCTION(calendar)
112 {
113 	register_calendar_symbols(module_number);
114 
115 	return SUCCESS;
116 }
117 
PHP_MINFO_FUNCTION(calendar)118 PHP_MINFO_FUNCTION(calendar)
119 {
120 	php_info_print_table_start();
121 	php_info_print_table_row(2, "Calendar support", "enabled");
122 	php_info_print_table_end();
123 }
124 
_php_cal_info(int cal,zval * ret)125 static void _php_cal_info(int cal, zval *ret)
126 {
127 	zval months, smonths;
128 	int i;
129 	const struct cal_entry_t *calendar;
130 
131 	calendar = &cal_conversion_table[cal];
132 	array_init(ret);
133 
134 	array_init(&months);
135 	array_init(&smonths);
136 
137 	for (i = 1; i <= calendar->num_months; i++) {
138 		add_index_string(&months, i, calendar->month_name_long[i]);
139 		add_index_string(&smonths, i, calendar->month_name_short[i]);
140 	}
141 
142 	add_assoc_zval(ret, "months", &months);
143 	add_assoc_zval(ret, "abbrevmonths", &smonths);
144 	add_assoc_long(ret, "maxdaysinmonth", calendar->max_days_in_month);
145 	add_assoc_string(ret, "calname", calendar->name);
146 	add_assoc_string(ret, "calsymbol", calendar->symbol);
147 
148 }
149 
150 /* {{{ Returns information about a particular calendar */
PHP_FUNCTION(cal_info)151 PHP_FUNCTION(cal_info)
152 {
153 	zend_long cal = -1;
154 
155 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "|l", &cal) == FAILURE) {
156 		RETURN_THROWS();
157 	}
158 
159 	if (cal == -1) {
160 		int i;
161 		zval val;
162 
163 		array_init(return_value);
164 
165 		for (i = 0; i < CAL_NUM_CALS; i++) {
166 			_php_cal_info(i, &val);
167 			add_index_zval(return_value, i, &val);
168 		}
169 		return;
170 	}
171 
172 	if (cal < 0 || cal >= CAL_NUM_CALS) {
173 		zend_argument_value_error(1, "must be a valid calendar ID");
174 		RETURN_THROWS();
175 	}
176 
177 	_php_cal_info(cal, return_value);
178 }
179 /* }}} */
180 
181 /* {{{ Returns the number of days in a month for a given year and calendar */
PHP_FUNCTION(cal_days_in_month)182 PHP_FUNCTION(cal_days_in_month)
183 {
184 	zend_long cal, month, year;
185 	const struct cal_entry_t *calendar;
186 	zend_long sdn_start, sdn_next;
187 
188 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "lll", &cal, &month, &year) == FAILURE) {
189 		RETURN_THROWS();
190 	}
191 
192 	if (cal < 0 || cal >= CAL_NUM_CALS) {
193 		zend_argument_value_error(1, "must be a valid calendar ID");
194 		RETURN_THROWS();
195 	}
196 
197 	calendar = &cal_conversion_table[cal];
198 
199 	sdn_start = calendar->to_jd(year, month, 1);
200 
201 	if (sdn_start == 0) {
202 		zend_value_error("Invalid date");
203 		RETURN_THROWS();
204 	}
205 
206 	sdn_next = calendar->to_jd(year, 1 + month, 1);
207 
208 	if (sdn_next == 0) {
209 		/* If the next month is invalid, then we need to try the first month of
210 		 * the next year, bearing in mind that the next year after 1 BCE is
211 		 * actually 1 AD and not 0. */
212 		if (year == -1) {
213 			sdn_next = calendar->to_jd(1, 1, 1);
214 		}
215 		else {
216 			sdn_next = calendar->to_jd(year + 1, 1, 1);
217 			if (cal == CAL_FRENCH && sdn_next == 0) {
218 				/* The French calendar ends on 0014-13-05. */
219 				sdn_next = 2380953;
220 			}
221 		}
222 	}
223 
224 	RETURN_LONG(sdn_next - sdn_start);
225 }
226 /* }}} */
227 
228 /* {{{ Converts from a supported calendar to Julian Day Count */
PHP_FUNCTION(cal_to_jd)229 PHP_FUNCTION(cal_to_jd)
230 {
231 	zend_long cal, month, day, year;
232 
233 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "llll", &cal, &month, &day, &year) != SUCCESS) {
234 		RETURN_THROWS();
235 	}
236 
237 	if (cal < 0 || cal >= CAL_NUM_CALS) {
238 		zend_argument_value_error(1, "must be a valid calendar ID");
239 		RETURN_THROWS();
240 	}
241 
242 	RETURN_LONG(cal_conversion_table[cal].to_jd(year, month, day));
243 }
244 /* }}} */
245 
246 /* {{{ Converts from Julian Day Count to a supported calendar and return extended information */
PHP_FUNCTION(cal_from_jd)247 PHP_FUNCTION(cal_from_jd)
248 {
249 	zend_long jd, cal;
250 	int month, day, year, dow;
251 	const struct cal_entry_t *calendar;
252 
253 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "ll", &jd, &cal) == FAILURE) {
254 		RETURN_THROWS();
255 	}
256 
257 	if (cal < 0 || cal >= CAL_NUM_CALS) {
258 		zend_argument_value_error(2, "must be a valid calendar ID");
259 		RETURN_THROWS();
260 	}
261 	calendar = &cal_conversion_table[cal];
262 
263 	array_init(return_value);
264 
265 	calendar->from_jd(jd, &year, &month, &day);
266 
267 	add_assoc_str(return_value, "date",
268 		zend_strpprintf(0, "%i/%i/%i", month, day, year));
269 
270 	add_assoc_long(return_value, "month", month);
271 	add_assoc_long(return_value, "day", day);
272 	add_assoc_long(return_value, "year", year);
273 
274 /* day of week */
275 	if (cal != CAL_JEWISH || year > 0) {
276 		dow = DayOfWeek(jd);
277 		add_assoc_long(return_value, "dow", dow);
278 		add_assoc_string(return_value, "abbrevdayname", DayNameShort[dow]);
279 		add_assoc_string(return_value, "dayname", DayNameLong[dow]);
280 	} else {
281 		add_assoc_null(return_value, "dow");
282 		add_assoc_string(return_value, "abbrevdayname", "");
283 		add_assoc_string(return_value, "dayname", "");
284 	}
285 /* month name */
286 	if(cal == CAL_JEWISH) {
287 		/* special case for Jewish calendar */
288 		add_assoc_string(return_value, "abbrevmonth", (year > 0 ? JEWISH_MONTH_NAME(year)[month] : ""));
289 		add_assoc_string(return_value, "monthname", (year > 0 ? JEWISH_MONTH_NAME(year)[month] : ""));
290 	} else {
291 		add_assoc_string(return_value, "abbrevmonth", calendar->month_name_short[month]);
292 		add_assoc_string(return_value, "monthname", calendar->month_name_long[month]);
293 	}
294 }
295 /* }}} */
296 
297 /* {{{ Converts a julian day count to a gregorian calendar date */
PHP_FUNCTION(jdtogregorian)298 PHP_FUNCTION(jdtogregorian)
299 {
300 	zend_long julday;
301 	int year, month, day;
302 
303 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &julday) == FAILURE) {
304 		RETURN_THROWS();
305 	}
306 
307 	SdnToGregorian(julday, &year, &month, &day);
308 
309 	RETURN_NEW_STR(zend_strpprintf(0, "%i/%i/%i", month, day, year));
310 }
311 /* }}} */
312 
313 /* {{{ Converts a gregorian calendar date to julian day count */
PHP_FUNCTION(gregoriantojd)314 PHP_FUNCTION(gregoriantojd)
315 {
316 	zend_long year, month, day;
317 
318 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "lll", &month, &day, &year) == FAILURE) {
319 		RETURN_THROWS();
320 	}
321 
322 	RETURN_LONG(GregorianToSdn(year, month, day));
323 }
324 /* }}} */
325 
326 /* {{{ Convert a julian day count to a julian calendar date */
PHP_FUNCTION(jdtojulian)327 PHP_FUNCTION(jdtojulian)
328 {
329 	zend_long julday;
330 	int year, month, day;
331 
332 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &julday) == FAILURE) {
333 		RETURN_THROWS();
334 	}
335 
336 	SdnToJulian(julday, &year, &month, &day);
337 
338 	RETURN_NEW_STR(zend_strpprintf(0, "%i/%i/%i", month, day, year));
339 }
340 /* }}} */
341 
342 /* {{{ Converts a julian calendar date to julian day count */
PHP_FUNCTION(juliantojd)343 PHP_FUNCTION(juliantojd)
344 {
345 	zend_long year, month, day;
346 
347 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "lll", &month, &day, &year) == FAILURE) {
348 		RETURN_THROWS();
349 	}
350 
351 	RETURN_LONG(JulianToSdn(year, month, day));
352 }
353 /* }}} */
354 
355 /* {{{ heb_number_to_chars*/
356 /*
357 caution: the Hebrew format produces non-unique result.
358 for example both: year '5' and year '5000' produce 'ה'.
359 use the numeric one for calculations.
360  */
heb_number_to_chars(int n,int fl,char ** ret)361 static char *heb_number_to_chars(int n, int fl, char **ret)
362 {
363 	char *p, old[18], *endofalafim;
364 
365 	p = endofalafim = old;
366 /*
367    prevents the option breaking the jewish beliefs, and some other
368    critical resources ;)
369  */
370 	if (n > 9999 || n < 1) {
371 		*ret = NULL;
372 		return NULL;
373 	}
374 
375 /* alafim (thousands) case */
376 	if (n / 1000) {
377 		*p = alef_bet[n / 1000];
378 		p++;
379 
380 		if (CAL_JEWISH_ADD_ALAFIM_GERESH & fl) {
381 			*p = '\'';
382 			p++;
383 		}
384 		if (CAL_JEWISH_ADD_ALAFIM & fl) {
385 			/* Escape sequences of Hebrew characters in ISO-8859-8: אלפים */
386 			strcpy(p, " \xE0\xEC\xF4\xE9\xED ");
387 			p += 7;
388 		}
389 
390 		endofalafim = p;
391 		n = n % 1000;
392 	}
393 
394 /* tav-tav (tav=400) case */
395 	while (n >= 400) {
396 		*p = alef_bet[22];
397 		p++;
398 		n -= 400;
399 	}
400 
401 /* meot (hundreds) case */
402 	if (n >= 100) {
403 		*p = alef_bet[18 + n / 100];
404 		p++;
405 		n = n % 100;
406 	}
407 
408 /* tet-vav & tet-zain case (special case for 15 and 16) */
409 	if (n == 15 || n == 16) {
410 		*p = alef_bet[9];
411 		p++;
412 		*p = alef_bet[n - 9];
413 		p++;
414 	} else {
415 /* asarot (tens) case */
416 		if (n >= 10) {
417 			*p = alef_bet[9 + n / 10];
418 			p++;
419 			n = n % 10;
420 		}
421 
422 /* yehidot (ones) case */
423 		if (n > 0) {
424 			*p = alef_bet[n];
425 			p++;
426 		}
427 	}
428 
429 	if (CAL_JEWISH_ADD_GERESHAYIM & fl) {
430 		switch (p - endofalafim) {
431 		case 0:
432 			break;
433 		case 1:
434 			*p = '\'';
435 			p++;
436 			break;
437 		default:
438 			*(p) = *(p - 1);
439 			*(p - 1) = '"';
440 			p++;
441 		}
442 	}
443 
444 	*p = '\0';
445 	*ret = estrndup(old, (p - old) + 1);
446 	p = *ret;
447 	return p;
448 }
449 /* }}} */
450 
451 /* {{{ Converts a julian day count to a jewish calendar date */
PHP_FUNCTION(jdtojewish)452 PHP_FUNCTION(jdtojewish)
453 {
454 	zend_long julday, fl = 0;
455 	bool heb   = 0;
456 	int year, month, day;
457 	char *dayp, *yearp;
458 
459 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "l|bl", &julday, &heb, &fl) == FAILURE) {
460 		RETURN_THROWS();
461 	}
462 
463 	SdnToJewish(julday, &year, &month, &day);
464 	if (!heb) {
465 		RETURN_NEW_STR(zend_strpprintf(0, "%i/%i/%i", month, day, year));
466 	} else {
467 		if (year <= 0 || year > 9999) {
468 			zend_value_error("Year out of range (0-9999)");
469 			RETURN_THROWS();
470 		}
471 
472 		RETVAL_NEW_STR(zend_strpprintf(0, "%s %s %s", heb_number_to_chars(day, fl, &dayp), JEWISH_HEB_MONTH_NAME(year)[month], heb_number_to_chars(year, fl, &yearp)));
473 
474 		if (dayp) {
475 			efree(dayp);
476 		}
477 		if (yearp) {
478 			efree(yearp);
479 		}
480 	}
481 }
482 /* }}} */
483 
484 /* {{{ Converts a jewish calendar date to a julian day count */
PHP_FUNCTION(jewishtojd)485 PHP_FUNCTION(jewishtojd)
486 {
487 	zend_long year, month, day;
488 
489 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "lll", &month, &day, &year) == FAILURE) {
490 		RETURN_THROWS();
491 	}
492 
493 	RETURN_LONG(JewishToSdn(year, month, day));
494 }
495 /* }}} */
496 
497 /* {{{ Converts a julian day count to a french republic calendar date */
PHP_FUNCTION(jdtofrench)498 PHP_FUNCTION(jdtofrench)
499 {
500 	zend_long julday;
501 	int year, month, day;
502 
503 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &julday) == FAILURE) {
504 		RETURN_THROWS();
505 	}
506 
507 	SdnToFrench(julday, &year, &month, &day);
508 
509 	RETURN_NEW_STR(zend_strpprintf(0, "%i/%i/%i", month, day, year));
510 }
511 /* }}} */
512 
513 /* {{{ Converts a french republic calendar date to julian day count */
PHP_FUNCTION(frenchtojd)514 PHP_FUNCTION(frenchtojd)
515 {
516 	zend_long year, month, day;
517 
518 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "lll", &month, &day, &year) == FAILURE) {
519 		RETURN_THROWS();
520 	}
521 
522 	RETURN_LONG(FrenchToSdn(year, month, day));
523 }
524 /* }}} */
525 
526 /* {{{ Returns name or number of day of week from julian day count */
PHP_FUNCTION(jddayofweek)527 PHP_FUNCTION(jddayofweek)
528 {
529 	zend_long julday, mode = CAL_DOW_DAYNO;
530 	int day;
531 	const char *daynamel, *daynames;
532 
533 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "l|l", &julday, &mode) == FAILURE) {
534 		RETURN_THROWS();
535 	}
536 
537 	day = DayOfWeek(julday);
538 	daynamel = DayNameLong[day];
539 	daynames = DayNameShort[day];
540 
541 	switch (mode) {
542 	case CAL_DOW_LONG:
543 		RETURN_STRING(daynamel);
544 		break;
545 	case CAL_DOW_SHORT:
546 		RETURN_STRING(daynames);
547 		break;
548 	case CAL_DOW_DAYNO:
549 	default:
550 		RETURN_LONG(day);
551 		break;
552 	}
553 }
554 /* }}} */
555 
556 /* {{{ Returns name of month for julian day count */
PHP_FUNCTION(jdmonthname)557 PHP_FUNCTION(jdmonthname)
558 {
559 	zend_long julday, mode;
560 	const char *monthname = NULL;
561 	int month, day, year;
562 
563 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "ll", &julday, &mode) == FAILURE) {
564 		RETURN_THROWS();
565 	}
566 
567 	switch (mode) {
568 	case CAL_MONTH_GREGORIAN_LONG:	/* gregorian or julian month */
569 		SdnToGregorian(julday, &year, &month, &day);
570 		monthname = MonthNameLong[month];
571 		break;
572 	case CAL_MONTH_JULIAN_SHORT:	/* gregorian or julian month */
573 		SdnToJulian(julday, &year, &month, &day);
574 		monthname = MonthNameShort[month];
575 		break;
576 	case CAL_MONTH_JULIAN_LONG:	/* gregorian or julian month */
577 		SdnToJulian(julday, &year, &month, &day);
578 		monthname = MonthNameLong[month];
579 		break;
580 	case CAL_MONTH_JEWISH:		/* jewish month */
581 		SdnToJewish(julday, &year, &month, &day);
582 		monthname = (year > 0 ? JEWISH_MONTH_NAME(year)[month] : "");
583 		break;
584 	case CAL_MONTH_FRENCH:		/* french month */
585 		SdnToFrench(julday, &year, &month, &day);
586 		monthname = FrenchMonthName[month];
587 		break;
588 	default:					/* default gregorian */
589 	case CAL_MONTH_GREGORIAN_SHORT:	/* gregorian or julian month */
590 		SdnToGregorian(julday, &year, &month, &day);
591 		monthname = MonthNameShort[month];
592 		break;
593 	}
594 
595 	RETURN_STRING(monthname);
596 }
597 /* }}} */
598