xref: /php-src/ext/calendar/easter.c (revision 8ef0e4cf)
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    |          Arne Perschke             <a.perschke@hctec.net>            |
17    +----------------------------------------------------------------------+
18  */
19 
20 #include "php.h"
21 #include "php_calendar.h"
22 #include "sdncal.h"
23 #include <time.h>
24 
25 /**
26  * If `gm` is true this will return the timestamp at midnight on Easter of the given year. If it is false this
27  * will return the number of days Easter is after March 21st.
28  */
_cal_easter(INTERNAL_FUNCTION_PARAMETERS,bool gm)29 static void _cal_easter(INTERNAL_FUNCTION_PARAMETERS, bool gm)
30 {
31 	/* based on code by Simon Kershaw, <webmaster@ely.anglican.org> */
32 
33 	struct tm te;
34 	zend_long year, golden, solar, lunar, pfm, dom, tmp, easter, result;
35 	zend_long method = CAL_EASTER_DEFAULT;
36 	bool year_is_null = 1;
37 
38 	if (zend_parse_parameters(ZEND_NUM_ARGS(),
39 		"|l!l", &year, &year_is_null, &method) == FAILURE) {
40 			RETURN_THROWS();
41 	}
42 
43 	/* Default to the current year if year parameter is not given */
44 	if (year_is_null) {
45 		time_t a;
46 		struct tm b, *res;
47 		time(&a);
48 		res = php_localtime_r(&a, &b);
49 		if (!res) {
50 			year = 1900;
51 		} else {
52 			year = 1900 + b.tm_year;
53 		}
54 	}
55 
56 	#ifdef ZEND_ENABLE_ZVAL_LONG64
57 	/* Compiling for 64bit, allow years between 1970 and 2.000.000.000 */
58 	if (gm && year < 1970) {
59 		/* timestamps only start after 1970 */
60 		zend_argument_value_error(1, "must be a year after 1970 (inclusive)");
61 		RETURN_THROWS();
62 	}
63 
64 	if (gm && year > 2000000000) {
65 		/* timestamps only go up to the year 2.000.000.000 */
66 		zend_argument_value_error(1, "must be a year before 2.000.000.000 (inclusive)");
67 		RETURN_THROWS();
68 	}
69 	#else
70 	/* Compiling for 32bit, allow years between 1970 and 2037 */
71 	if (gm && (year < 1970 || year > 2037)) {
72 		zend_argument_value_error(1, "must be between 1970 and 2037 (inclusive)");
73 		RETURN_THROWS();
74 	}
75 	#endif
76 
77 
78 	golden = (year % 19) + 1;					/* the Golden number */
79 
80 	if ((year <= 1582 && method != CAL_EASTER_ALWAYS_GREGORIAN) ||
81 	    (year >= 1583 && year <= 1752 && method != CAL_EASTER_ROMAN && method != CAL_EASTER_ALWAYS_GREGORIAN) ||
82 	     method == CAL_EASTER_ALWAYS_JULIAN) {		/* JULIAN CALENDAR */
83 
84 		dom = (year + (year/4) + 5) % 7;			/* the "Dominical number" - finding a Sunday */
85 		if (dom < 0) {
86 			dom += 7;
87 		}
88 
89 		pfm = (3 - (11*golden) - 7) % 30;			/* uncorrected date of the Paschal full moon */
90 		if (pfm < 0) {
91 			pfm += 30;
92 		}
93 	} else {							/* GREGORIAN CALENDAR */
94 		dom = (year + (year/4) - (year/100) + (year/400)) % 7;	/* the "Domincal number" */
95 		if (dom < 0) {
96 			dom += 7;
97 		}
98 
99 		solar = (year-1600)/100 - (year-1600)/400;		/* the solar and lunar corrections */
100 		lunar = (((year-1400) / 100) * 8) / 25;
101 
102 		pfm = (3 - (11*golden) + solar - lunar) % 30;		/* uncorrected date of the Paschal full moon */
103 		if (pfm < 0) {
104 			pfm += 30;
105 		}
106 	}
107 
108 	if ((pfm == 29) || (pfm == 28 && golden > 11)) {		/* corrected date of the Paschal full moon */
109 		pfm--;							/* - days after 21st March                 */
110 	}
111 
112 	tmp = (4-pfm-dom) % 7;
113 	if (tmp < 0) {
114 		tmp += 7;
115 	}
116 
117 	easter = pfm + tmp + 1;	    					/* Easter as the number of days after 21st March */
118 
119 	if (gm) {							/* return a timestamp */
120 		te.tm_isdst = -1;
121 		te.tm_year = year-1900;
122 		te.tm_sec = 0;
123 		te.tm_min = 0;
124 		te.tm_hour = 0;
125 
126 		if (easter < 11) {
127 			te.tm_mon = 2;			/* March */
128 			te.tm_mday = easter+21;
129 		} else {
130 			te.tm_mon = 3;			/* April */
131 			te.tm_mday = easter-10;
132 		}
133 	    result = mktime(&te);
134 	} else {							/* return the days after March 21 */
135 	    result = easter;
136 	}
137 	ZVAL_LONG(return_value, result);
138 }
139 
140 /* {{{ Return the timestamp of midnight on Easter of a given year (defaults to current year) */
PHP_FUNCTION(easter_date)141 PHP_FUNCTION(easter_date)
142 {
143 	_cal_easter(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
144 }
145 /* }}} */
146 
147 /* {{{ Return the number of days after March 21 that Easter falls on for a given year (defaults to current year) */
PHP_FUNCTION(easter_days)148 PHP_FUNCTION(easter_days)
149 {
150 	_cal_easter(INTERNAL_FUNCTION_PARAM_PASSTHRU, false);
151 }
152 /* }}} */
153