Skip to content

Commit

Permalink
[zend-date] properly calculate sunrise, sunset and twilight times (#151)
Browse files Browse the repository at this point in the history
* [zend-date] fix calculating sunrise, sunset and twilight times

When php 8.1.0 deprecated date_sunset() and date_sunrise() functions,
switched to recommended date_sun_info() function is used instead (which is available in php since v5.1)
but that function yields slightly different results since zenith angles are internally fixed
and they are different to what zf used before. (see $horizonDeclination in Zend_Date::calcSun() - moved from Zend_Date::_checkLocation())

Yet ONLY NOW they are accurate! (calculations for civil / nautical / astronomical twilight were totally wrong before)

Still values returned by date_sun_info() are slightly different in php 8.0+, (but only for sunrise/sunset, not for twilight)
so yet another set of conditions have to be added to test suites.
  • Loading branch information
falkenhawk committed Dec 6, 2022
1 parent 37d353f commit 1dd472b
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 144 deletions.
40 changes: 13 additions & 27 deletions packages/zend-date/library/Zend/Date.php
Original file line number Diff line number Diff line change
Expand Up @@ -3304,23 +3304,8 @@ private function _checkLocation($location)
throw new Zend_Date_Exception('Latitude must be between -90 and 90', 0, null, $location);
}

if (!isset($location['horizon'])){
$location['horizon'] = 'effective';
}

switch ($location['horizon']) {
case 'civil' :
return -0.104528;
break;
case 'nautic' :
return -0.207912;
break;
case 'astronomic' :
return -0.309017;
break;
default :
return -0.0145439;
break;
if (isset($location['horizon']) && !in_array($location['horizon'], array('civil', 'nautic', 'astronomic', 'effective'))) {
throw new Zend_Date_Exception('Invalid value for \'horizon\', must be one of: \'civil\', \'nautic\', \'astronomic\', \'effective\'', 0, null, $location);
}
}

Expand All @@ -3330,17 +3315,17 @@ private function _checkLocation($location)
* For a list of cities and correct locations use the class Zend_Date_Cities
*
* @param array $location location of sunrise
* ['horizon'] -> civil, nautic, astronomical, effective (default)
* ['horizon'] -> (optional) civil, nautic, astronomic, effective (default)
* ['longitude'] -> longitude of location
* ['latitude'] -> latitude of location
* @return Zend_Date
* @throws Zend_Date_Exception
*/
public function getSunrise($location)
{
$horizon = $this->_checkLocation($location);
$this->_checkLocation($location);
$result = clone $this;
$result->set($this->calcSun($location, $horizon, true), self::TIMESTAMP);
$result->set($this->calcSun($location, true), self::TIMESTAMP);
return $result;
}

Expand All @@ -3350,17 +3335,17 @@ public function getSunrise($location)
* For a list of cities and correct locations use the class Zend_Date_Cities
*
* @param array $location location of sunset
* ['horizon'] -> civil, nautic, astronomical, effective (default)
* ['horizon'] -> (optional) civil, nautic, astronomic, effective (default)
* ['longitude'] -> longitude of location
* ['latitude'] -> latitude of location
* @return Zend_Date
* @throws Zend_Date_Exception
*/
public function getSunset($location)
{
$horizon = $this->_checkLocation($location);
$this->_checkLocation($location);
$result = clone $this;
$result->set($this->calcSun($location, $horizon, false), self::TIMESTAMP);
$result->set($this->calcSun($location, false), self::TIMESTAMP);
return $result;
}

Expand All @@ -3370,7 +3355,7 @@ public function getSunset($location)
* For a list of cities and correct locations use the class Zend_Date_Cities
*
* @param array $location location of suninfo
* ['horizon'] -> civil, nautic, astronomical, effective (default)
* ['horizon'] -> (optional) civil, nautic, astronomic, effective (default)
* ['longitude'] -> longitude of location
* ['latitude'] -> latitude of location
* @return array - [sunset|sunrise][effective|civil|nautic|astronomic]
Expand All @@ -3394,12 +3379,13 @@ public function getSunInfo($location)
$location['horizon'] = 'astronomic';
break;
}
$horizon = $this->_checkLocation($location);
$this->_checkLocation($location);

$result = clone $this;
$result->set($this->calcSun($location, $horizon, true), self::TIMESTAMP);
$result->set($this->calcSun($location, true), self::TIMESTAMP);
$suninfo['sunrise'][$location['horizon']] = $result;
$result = clone $this;
$result->set($this->calcSun($location, $horizon, false), self::TIMESTAMP);
$result->set($this->calcSun($location, false), self::TIMESTAMP);
$suninfo['sunset'][$location['horizon']] = $result;
}
return $suninfo;
Expand Down
8 changes: 5 additions & 3 deletions packages/zend-date/library/Zend/Date/Cities.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,18 +291,20 @@ class Zend_Date_Cities
* Returns the location from the selected city
*
* @param string $city City to get location for
* @param string $horizon Horizon to use :
* @param string|null $horizon Horizon to use :
* default: effective
* others are civil, nautic, astronomic
* @return array
* @throws Zend_Date_Exception When city is unknown
*/
public static function City($city, $horizon = false)
public static function City($city, $horizon = null)
{
foreach (self::$cities as $key => $value) {
if (strtolower($key) === strtolower($city)) {
$return = $value;
$return['horizon'] = $horizon;
if ($horizon !== null) {
$return['horizon'] = $horizon;
}
return $return;
}
}
Expand Down
48 changes: 39 additions & 9 deletions packages/zend-date/library/Zend/Date/DateObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -906,20 +906,31 @@ private function _range($a, $b) {
/**
* Calculates the sunrise or sunset based on a location
*
* @param array $location Location for calculation MUST include 'latitude', 'longitude', 'horizon'
* @param bool $horizon true: sunrise; false: sunset
* @param array $location Location for calculation MUST include 'latitude', 'longitude'
* and optional 'horizon' (civil, nautic, astronomical or effective [default])
* @param bool $rise true: sunrise; false: sunset
* @return mixed - false: midnight sun, integer:
*/
protected function calcSun($location, $horizon, $rise = false)
protected function calcSun($location, $rise = false)
{
$horizon = isset($location['horizon']) ? $location['horizon'] : 'effective';

// timestamp within 32bit
if (abs($this->_unixTimestamp) <= 0x7FFFFFFF) {
if ($rise === false) {
return date_sunset($this->_unixTimestamp, SUNFUNCS_RET_TIMESTAMP, $location['latitude'],
$location['longitude'], 90 + $horizon, $this->getGmtOffset() / 3600);
$suninfo = date_sun_info($this->_unixTimestamp, $location['latitude'], $location['longitude']);
$prop = $rise ? 'sunrise' : 'sunset';
switch ($horizon) {
case 'civil':
$prop = $rise ? 'civil_twilight_begin' : 'civil_twilight_end';
break;
case 'nautic':
$prop = $rise ? 'nautical_twilight_begin' : 'nautical_twilight_end';
break;
case 'astronomic':
$prop = $rise ? 'astronomical_twilight_begin' : 'astronomical_twilight_end';
break;
}
return date_sunrise($this->_unixTimestamp, SUNFUNCS_RET_TIMESTAMP, $location['latitude'],
$location['longitude'], 90 + $horizon, $this->getGmtOffset() / 3600);
return $suninfo[$prop];
}

// self calculation - timestamp bigger than 32bit
Expand Down Expand Up @@ -965,7 +976,26 @@ protected function calcSun($location, $horizon, $rise = false)
$solDeclination /= sqrt(-$solDeclination * $solDeclination + 1);
$solDeclination = atan2($solDeclination, 1);

$solHorizon = $horizon - sin($solDeclination) * sin($radLatitude);
// Even though horizon declinations should rather be set to (minus) radian values of 0.35', 6, 12, 18 degrees respectively,
// the following values were used to be in sync with the old date_sunrise() and date_sunset() functions
// and they are taken from original sources by William C. Bell / Chris Spratt et. al.
// https://github.com/hollie/misterhouse/blob/67b4e36de6fccfb159c6b04c2f3e4b238ad23b5b/bin/sun_time#L97-L100
// and were defined exactly like that in Zend_Date::_checkLocation() - they are now moved here.
$horizonDeclination = -0.0145439;
switch ($horizon) {
case 'civil':
$horizonDeclination = -0.104528;
break;
case 'nautic':
$horizonDeclination = -0.207912;
break;
case 'astronomic':
$horizonDeclination = -0.309017;
break;
}

$solHorizon = $horizonDeclination - sin($solDeclination) * sin($radLatitude);
//$solHorizon = $horizon - sin($solDeclination) * sin($radLatitude);
$solHorizon /= cos($solDeclination) * cos($radLatitude);

// midnight sun, always night
Expand Down
Loading

0 comments on commit 1dd472b

Please sign in to comment.