├── .github └── workflows │ └── ci.yml ├── .gitignore ├── ChangeLog-2.1.md ├── ChangeLog-3.0.md ├── ChangeLog-3.1.md ├── ChangeLog-3.2.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json ├── ecs.php ├── example └── example1.php ├── phpstan.neon ├── phpunit.xml ├── rector.php ├── src └── MoonPhase.php └── tests └── MoonPhaseTest.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-20.04 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Validate composer.json and composer.lock 18 | run: composer validate --strict 19 | 20 | - name: Cache Composer packages 21 | id: composer-cache 22 | uses: actions/cache@v3 23 | with: 24 | path: vendor 25 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-php- 28 | 29 | - name: Install dependencies 30 | run: composer install --prefer-dist --no-progress 31 | 32 | - name: Run PHPUnit 33 | env: 34 | XDEBUG_MODE: coverage 35 | run: composer run-script phpunit 36 | 37 | - name: Run PHPStan 38 | run: composer run-script phpstan -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor 3 | .phpunit.result.cache 4 | /tests/build 5 | composer.lock 6 | .DS_Store 7 | /.phpunit.cache 8 | -------------------------------------------------------------------------------- /ChangeLog-2.1.md: -------------------------------------------------------------------------------- 1 | # Changes in Solaris Moon Phase v2.1 2 | 3 | ## 2.1.0 2022-05-20 4 | 5 | ### Changed 6 | 7 | - A lot of method names have been refactored to match the coding conventions of [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-12](https://www.php-fig.org/psr/psr-12/). For example `phase_name()` is now `getPhaseName()`. Methods with parameters have been split, for example `get_phase('new_moon')` is now `getPhaseNewMoon()`. Old methods are still available and can be used anyway. They will be removed in `v3.0`. To inform about those changes, deprecation messages have been added. -------------------------------------------------------------------------------- /ChangeLog-3.0.md: -------------------------------------------------------------------------------- 1 | # Changes in Solaris Moon Phase v3.0 2 | 3 | ## 3.0.0 2023-04-11 4 | 5 | ### Changed 6 | 7 | - The library now requires at least PHP `8.2`. 8 | - Some internal methods and properties have been renamed. 9 | - Deprecated methods have been removed. -------------------------------------------------------------------------------- /ChangeLog-3.1.md: -------------------------------------------------------------------------------- 1 | # Changes in Solaris Moon Phase v3.1 2 | 3 | ## 3.1.1 2024-11-06 4 | 5 | ### Changed 6 | 7 | - Improved support for PHP 8.4 ([#44](https://github.com/BitAndBlack/php-moon-phase/pull/44)). 8 | 9 | ## 3.1.0 2024-09-26 10 | 11 | ### Changed 12 | 13 | - Development dependencies have been updated. -------------------------------------------------------------------------------- /ChangeLog-3.2.md: -------------------------------------------------------------------------------- 1 | # Changes in Solaris Moon Phase v3.2 2 | 3 | ## 3.2.0 2024-11-07 4 | 5 | ### Changed 6 | 7 | - Replaced constructor parameter #1 of type `DateTime` with type `DateTimeInterface` ([#45](https://github.com/BitAndBlack/php-moon-phase/pull/45)). 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | php-moon-phase - A PHP class for calculating the phase of the Moon, and other related variables. 2 | 3 | Copyright 2012 Samir Shah, < samir [at] rayofsolaris.net >, http://rayofsolaris.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PHP from Packagist](https://img.shields.io/packagist/php-v/solaris/php-moon-phase)](http://www.php.net) 2 | [![Latest Stable Version](https://poser.pugx.org/solaris/php-moon-phase/v/stable)](https://packagist.org/packages/solaris/php-moon-phase) 3 | [![Total Downloads](https://poser.pugx.org/solaris/php-moon-phase/downloads)](https://packagist.org/packages/solaris/php-moon-phase) 4 | [![License](https://poser.pugx.org/solaris/php-moon-phase/license)](https://packagist.org/packages/solaris/php-moon-phase) 5 | 6 | # Solaris PHP Moon Phase 7 | 8 | Calculate the phases of the Moon in PHP. This library is based on [Moontool for Windows](http://www.fourmilab.ch/moontoolw/). 9 | 10 | ## Installation 11 | 12 | This library is made for the use with [Composer](https://packagist.org/packages/solaris/php-moon-phase). Add it to your project by running `$ composer require solaris/php-moon-phase`. 13 | 14 | ## Usage 15 | 16 | Create an instance of the `MoonPhase` class, supplying a `DateTime` object with a UNIX timestamp for when you want to determine the moon phase (if you don't then the current time will be used). 17 | 18 | You can then use the following methods: 19 | 20 | - `getPhase()`: the terminator phase angle as a fraction of a full circle (i.e., `0` to `1`). Both `0` and `1` correspond to a New Moon, and `0.5` corresponds to a Full Moon. 21 | - `getIllumination()`: the illuminated fraction of the Moon (`0` = New, `1` = Full). 22 | - `getAge()`: the age of the Moon, in days. 23 | - `getDistance()`: the distance of the Moon from the centre of the Earth (kilometres). 24 | - `getDiameter()`: the angular diameter subtended by the Moon as seen by an observer at the centre of the Earth (degrees). 25 | - `getSunDistance()`: the distance to the Sun (kilometres). 26 | - `getSunDiameter()`: the angular diameter subtended by the Sun as seen by an observer at the centre of the Earth (degrees). 27 | - `getPhaseNewMoon()`: the time of the New Moon in the current lunar cycle, i.e., the start of the current cycle (UNIX timestamp). 28 | - `getPhaseNextNewMoon()`: the time of the New Moon in the next lunar cycle, i.e., the start of the next cycle (UNIX timestamp). 29 | - `getPhaseFullMoon()`: the time of the Full Moon in the current lunar cycle (UNIX timestamp). 30 | - `getPhaseNextFullMoon()`: the time of the Full Moon in the next lunar cycle (UNIX timestamp). 31 | - `getPhaseFirstQuarter()`: the time of the first quarter in the current lunar cycle (UNIX timestamp). 32 | - `getPhaseNextFirstQuarter()`: the time of the first quarter in the next lunar cycle (UNIX timestamp). 33 | - `getPhaseLastQuarter()`: the time of the last quarter in the current lunar cycle (UNIX timestamp). 34 | - `getPhaseNextLastQuarter()`: the time of the last quarter in the next lunar cycle (UNIX timestamp). 35 | - `getPhaseName()`: the [phase name](https://aa.usno.navy.mil/faq/moon_phases). 36 | 37 | ### Example 38 | 39 | ```php 40 | getAge(), 1); 47 | $stage = $moonPhase->getPhase() < 0.5 ? 'waxing' : 'waning'; 48 | $distance = round($moonPhase->getDistance(), 2); 49 | $next = gmdate('G:i:s, j M Y', (int) $moonPhase->getPhaseNextNewMoon()); 50 | 51 | echo 'The moon is currently ' . $age . ' days old, and is therefore ' . $stage . '. '; 52 | echo 'It is ' . $distance . ' km from the centre of the Earth. '; 53 | echo 'The next new moon is at ' . $next . '. '; 54 | ``` 55 | 56 | ## Help 57 | 58 | If you have any questions, feel free to contact us under `hello@bitandblack.com`. 59 | 60 | Further information about Bit&Black can be found under [www.bitandblack.com](https://www.bitandblack.com). 61 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrades 2 | 3 | ## 2.x to 3.0 4 | 5 | In `v3.0` some internal methods and properties have been renamed. This should only affect you, if you are extending the MoonPhase class. 6 | 7 | Additionally, some deprecated methods have been removed. If you were still using them, please make sure to 8 | 9 | - replace `phase_name()` with `getPhaseName()`, 10 | - change `get_phase()` with one of its replacing getters. For example if you are using `get_phase('new_moon')`, the new method is `getPhaseNewMoon()`, 11 | - replace `get()` with another getter method, 12 | - replace `phase()` with `getPhase()`. 13 | 14 | The read more about those changes, see [ChangeLog-2.1.md](./ChangeLog-2.1.md). 15 | 16 | ## 1.x to 2.0 17 | 18 | The `v2.0` release does not add any new features and does not remove any. Instead, some changes have been made to bring the library up to current standards: 19 | 20 | - The PSR-0 autoloader has been replaced by the PSR-4 autoloader. 21 | - The directory structure has been adapted to PSR-4. 22 | - Tests with PHPUnit have been added to ensure safe development. 23 | - PHPStan was also added to perform static analysis. 24 | 25 | If you include the MoonPhase class manually, please update the path. 26 | 27 | ### Constructor 28 | 29 | Please note, that the constructor changed in `v2.0` so it expects a `DateTime` object (or no argument at all). If you used to initialize the `MoonPhase` class with a timestamp, make sure to convert it into a `DateTime` object before. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solaris/php-moon-phase", 3 | "description": "Calculate the phases of the Moon in PHP.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "php", 8 | "moon", 9 | "phase" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Samir Shah", 14 | "email": "samir@rayofsolaris.net" 15 | }, 16 | { 17 | "name": "Erik Scheepers", 18 | "email": "e.scheepers@linkorb.com", 19 | "role": "Packaging for composer" 20 | }, 21 | { 22 | "name": "Tobias Köngeter", 23 | "email": "hello@bitandblack.com", 24 | "homepage": "https://www.bitandblack.com" 25 | } 26 | ], 27 | "homepage": "https://github.com/BitAndBlack/php-moon-phase", 28 | "funding": [ 29 | { 30 | "type": "buymeacoffee", 31 | "url": "https://www.buymeacoffee.com/tobiaskoengeter" 32 | } 33 | ], 34 | "require": { 35 | "php": ">=8.2" 36 | }, 37 | "require-dev": { 38 | "phpstan/phpstan": "^1.0", 39 | "phpunit/phpunit": "^11.0", 40 | "rector/rector": "^1.0", 41 | "symplify/easy-coding-standard": "^12.0" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Solaris\\": "src/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "Solaris\\Tests\\": "tests/" 51 | } 52 | }, 53 | "config": { 54 | "sort-packages": true 55 | }, 56 | "scripts": { 57 | "phpstan": "php vendor/bin/phpstan analyse --configuration ./phpstan.neon --memory-limit=-1 --ansi", 58 | "phpunit": "php vendor/bin/phpunit --configuration ./phpunit.xml --colors=always", 59 | "refactor": "php vendor/bin/rector && php vendor/bin/ecs --fix" 60 | }, 61 | "scripts-descriptions": { 62 | "phpstan": "Runs PHPStan over the src folder and the tests folder.", 63 | "phpunit": "Runs PHPUnit.", 64 | "refactor": "Runs tools to refactor the code." 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | withParallel() 9 | ->withPaths([ 10 | __DIR__, 11 | ]) 12 | ->withSkip([ 13 | __DIR__ . DIRECTORY_SEPARATOR . 'vendor', 14 | ]) 15 | ->withSets([ 16 | SetList::PSR_12, 17 | SetList::ARRAY, 18 | SetList::CLEAN_CODE, 19 | ]) 20 | ->withConfiguredRule(YodaStyleFixer::class, [ 21 | 'always_move_variable' => true, 22 | ]) 23 | ; 24 | -------------------------------------------------------------------------------- /example/example1.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Tobias Köngeter 9 | * @copyright Copyright © Bit&Black 10 | * @link https://www.bitandblack.com 11 | * @license MIT 12 | */ 13 | 14 | use Solaris\MoonPhase; 15 | 16 | require_once dirname(__FILE__, 2) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; 17 | 18 | $moonPhase = new MoonPhase(); 19 | 20 | $age = round($moonPhase->getAge(), 1); 21 | $stage = $moonPhase->getPhase() < 0.5 ? 'waxing' : 'waning'; 22 | $distance = round($moonPhase->getDistance(), 2); 23 | $next = gmdate('G:i:s, j M Y', (int) $moonPhase->getPhaseNextNewMoon()); 24 | 25 | echo 'The moon is currently ' . $age . ' days old, and is therefore ' . $stage . '. '; 26 | echo 'It is ' . $distance . ' km from the centre of the Earth. '; 27 | echo 'The next new moon is at ' . $next . '. '; 28 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | - tests -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | tests/ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withParallel() 9 | ->withPaths([ 10 | __DIR__, 11 | ]) 12 | ->withSkip([ 13 | __DIR__ . DIRECTORY_SEPARATOR . 'vendor', 14 | PreferPHPUnitThisCallRector::class, 15 | ]) 16 | ->withSets([ 17 | PHPUnitSetList::PHPUNIT_110, 18 | PHPUnitSetList::PHPUNIT_CODE_QUALITY, 19 | ]) 20 | ->withImportNames() 21 | ->withPhpSets() 22 | ; 23 | -------------------------------------------------------------------------------- /src/MoonPhase.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Tobias Köngeter 9 | * @copyright Copyright © Bit&Black 10 | * @link https://www.bitandblack.com 11 | * @license MIT 12 | */ 13 | 14 | namespace Solaris; 15 | 16 | use DateTimeInterface; 17 | 18 | /** 19 | * @see \Solaris\Tests\MoonPhaseTest 20 | */ 21 | class MoonPhase 22 | { 23 | protected int $timestamp; 24 | 25 | protected float $phase; 26 | 27 | protected float $illumination; 28 | 29 | protected float $age; 30 | 31 | protected float $distance; 32 | 33 | protected float $diameter; 34 | 35 | protected float $sunDistance; 36 | 37 | protected float $sunDiameter; 38 | 39 | protected float $synmonth; 40 | 41 | /** 42 | * @var array|null 43 | */ 44 | protected ?array $quarters = null; 45 | 46 | protected float $ageDegrees; 47 | 48 | /** 49 | * @param DateTimeInterface|null $date 50 | */ 51 | public function __construct(?DateTimeInterface $date = null) 52 | { 53 | $date = null !== $date 54 | ? $date->getTimestamp() 55 | : time() 56 | ; 57 | 58 | $this->timestamp = $date; 59 | 60 | // Astronomical constants. 1980 January 0.0 61 | $epoch = 2_444_238.5; 62 | 63 | 64 | // Constants defining the Sun's apparent orbit 65 | 66 | // Ecliptic longitude of the Sun at epoch 1980.0 67 | $elonge = 278.833540; 68 | 69 | // Ecliptic longitude of the Sun at perigee 70 | $elongp = 282.596403; 71 | 72 | // Eccentricity of Earth's orbit 73 | $eccent = 0.016718; 74 | 75 | // Semi-major axis of Earth's orbit, km 76 | $sunsmax = 1.495985e8; 77 | 78 | // Sun's angular size, degrees, at semi-major axis distance 79 | $sunangsiz = 0.533128; 80 | 81 | 82 | // Elements of the Moon's orbit, epoch 1980.0 83 | 84 | // Moon's mean longitude at the epoch 85 | $mmlong = 64.975464; 86 | 87 | // Mean longitude of the perigee at the epoch 88 | $mmlongp = 349.383063; 89 | 90 | // Mean longitude of the node at the epoch 91 | // $mlnode = 151.950429; 92 | 93 | // Inclination of the Moon's orbit 94 | // $minc = 5.145396; 95 | 96 | // Eccentricity of the Moon's orbit 97 | $mecc = 0.054900; 98 | 99 | // Moon's angular size at distance a from Earth 100 | $mangsiz = 0.5181; 101 | 102 | // Semi-major axis of Moon's orbit in km 103 | $msmax = 384401; 104 | 105 | // Parallax at distance a from Earth 106 | // $mparallax = 0.9507; 107 | 108 | // Synodic month (new Moon to new Moon) 109 | $synmonth = 29.53058868; 110 | 111 | $this->synmonth = $synmonth; 112 | 113 | // date is coming in as a UNIX timstamp, so convert it to Julian 114 | $date = $date / 86400 + 2_440_587.5; 115 | 116 | 117 | // Calculation of the Sun's position 118 | 119 | // Date within epoch 120 | $day = $date - $epoch; 121 | 122 | // Mean anomaly of the Sun 123 | $n = $this->fixAngle((360 / 365.2422) * $day); 124 | 125 | // Convert from perigee co-ordinates to epoch 1980.0 126 | $m = $this->fixAngle($n + $elonge - $elongp); 127 | 128 | // Solve equation of Kepler 129 | $ec = $this->kepler($m, $eccent); 130 | $ec = sqrt((1 + $eccent) / (1 - $eccent)) * tan($ec / 2); 131 | 132 | // True anomaly 133 | $ec = 2 * rad2deg(atan($ec)); 134 | 135 | // Sun's geocentric ecliptic longitude 136 | $lambdaSun = $this->fixAngle($ec + $elongp); 137 | 138 | // Orbital distance factor 139 | $f = ((1 + $eccent * cos(deg2rad($ec))) / (1 - $eccent * $eccent)); 140 | 141 | // Distance to Sun in km 142 | $sunDist = $sunsmax / $f; 143 | 144 | // Sun's angular size in degrees 145 | $sunAng = $f * $sunangsiz; 146 | 147 | 148 | // Calculation of the Moon's position 149 | 150 | // Moon's mean longitude 151 | $ml = $this->fixAngle(13.1763966 * $day + $mmlong); 152 | 153 | // Moon's mean anomaly 154 | $mm = $this->fixAngle($ml - 0.1114041 * $day - $mmlongp); 155 | 156 | // Moon's ascending node mean longitude 157 | // $MN = $this->fixangle($mlnode - 0.0529539 * $day); 158 | 159 | $evection = 1.2739 * sin(deg2rad(2 * ($ml - $lambdaSun) - $mm)); 160 | 161 | $annualEquation = 0.1858 * sin(deg2rad($m)); 162 | 163 | // Correction term 164 | $a3 = 0.37 * sin(deg2rad($m)); 165 | 166 | // Corrected anomaly 167 | $mmp = $mm + $evection - $annualEquation - $a3; 168 | 169 | // Correction for the equation of the centre 170 | $mEc = 6.2886 * sin(deg2rad($mmp)); 171 | 172 | // Another correction term 173 | $a4 = 0.214 * sin(deg2rad(2 * $mmp)); 174 | 175 | // Corrected longitude 176 | $lP = $ml + $evection + $mEc - $annualEquation + $a4; 177 | 178 | $variation = 0.6583 * sin(deg2rad(2 * ($lP - $lambdaSun))); 179 | 180 | // True longitude 181 | $lPP = $lP + $variation; 182 | 183 | // Corrected longitude of the node 184 | // $NP = $MN - 0.16 * sin(deg2rad($m)); 185 | 186 | // Y inclination coordinate 187 | // $y = sin(deg2rad($lPP - $NP)) * cos(deg2rad($minc)); 188 | 189 | // X inclination coordinate 190 | // $x = cos(deg2rad($lPP - $NP)); 191 | 192 | // Ecliptic longitude 193 | // $Lambdamoon = rad2deg(atan2($y, $x)) + $NP; 194 | 195 | // Ecliptic latitude 196 | // $BetaM = rad2deg(asin(sin(deg2rad($lPP - $NP)) * sin(deg2rad($minc)))); 197 | 198 | 199 | // Calculation of the phase of the Moon 200 | 201 | // Age of the Moon in degrees 202 | $moonAge = $lPP - $lambdaSun; 203 | 204 | // Phase of the Moon 205 | $moonPhase = (1 - cos(deg2rad($moonAge))) / 2; 206 | 207 | // Distance of moon from the centre of the Earth 208 | $moonDist = ($msmax * (1 - $mecc * $mecc)) / (1 + $mecc * cos(deg2rad($mmp + $mEc))); 209 | 210 | $moonDFrac = $moonDist / $msmax; 211 | 212 | // Moon's angular diameter 213 | $moonAng = $mangsiz / $moonDFrac; 214 | 215 | // Moon's parallax 216 | // $MoonPar = $mparallax / $moonDFrac; 217 | 218 | 219 | // Store results 220 | 221 | // Phase (0 to 1) 222 | $this->phase = $this->fixAngle($moonAge) / 360; 223 | 224 | // Illuminated fraction (0 to 1) 225 | $this->illumination = $moonPhase; 226 | 227 | // Age of moon (days) 228 | $this->age = $synmonth * $this->phase; 229 | 230 | // Distance (kilometres) 231 | $this->distance = $moonDist; 232 | 233 | // Angular diameter (degrees) 234 | $this->diameter = $moonAng; 235 | 236 | // Age of the Moon in degrees 237 | $this->ageDegrees = $moonAge; 238 | 239 | // Distance to Sun (kilometres) 240 | $this->sunDistance = $sunDist; 241 | 242 | // Sun's angular diameter (degrees) 243 | $this->sunDiameter = $sunAng; 244 | } 245 | 246 | /** 247 | * Fix angle 248 | */ 249 | protected function fixAngle(float $angle): float 250 | { 251 | return $angle - 360 * floor($angle / 360); 252 | } 253 | 254 | /** 255 | * Kepler 256 | */ 257 | protected function kepler(float $m, float $ecc): float 258 | { 259 | // 1E-6 260 | $epsilon = 0.000001; 261 | $e = $m = deg2rad($m); 262 | 263 | do { 264 | $delta = $e - $ecc * sin($e) - $m; 265 | $e -= $delta / (1 - $ecc * cos($e)); 266 | } while (abs($delta) > $epsilon); 267 | 268 | return $e; 269 | } 270 | 271 | /** 272 | * Calculates time of the mean new Moon for a given base date. 273 | * This argument K to this function is the precomputed synodic month index, given by: 274 | * K = (year - 1900) * 12.3685 275 | * where year is expressed as a year and fractional year. 276 | */ 277 | protected function meanPhase(int $date, float $k): float 278 | { 279 | // Time in Julian centuries from 1900 January 0.5 280 | $jt = ($date - 2_415_020.0) / 36525; 281 | $t2 = $jt * $jt; 282 | $t3 = $t2 * $jt; 283 | 284 | $nt1 = 2_415_020.75933 + $this->synmonth * $k 285 | + 0.0001178 * $t2 286 | - 0.000000155 * $t3 287 | + 0.00033 * sin(deg2rad(166.56 + 132.87 * $jt - 0.009173 * $t2)) 288 | ; 289 | 290 | return $nt1; 291 | } 292 | 293 | /** 294 | * Given a K value used to determine the mean phase of the new moon and a 295 | * phase selector (0.0, 0.25, 0.5, 0.75), obtain the true, corrected phase time. 296 | */ 297 | protected function truePhase(float $k, float $phase): ?float 298 | { 299 | $apcor = false; 300 | 301 | // Add phase to new moon time 302 | $k += $phase; 303 | 304 | // Time in Julian centuries from 1900 January 0.5 305 | $t = $k / 1236.85; 306 | 307 | // Square for frequent use 308 | $t2 = $t * $t; 309 | 310 | // Cube for frequent use 311 | $t3 = $t2 * $t; 312 | 313 | // Mean time of phase 314 | $pt = 2_415_020.75933 315 | + $this->synmonth * $k 316 | + 0.0001178 * $t2 317 | - 0.000000155 * $t3 318 | + 0.00033 * sin(deg2rad(166.56 + 132.87 * $t - 0.009173 * $t2)) 319 | ; 320 | 321 | // Sun's mean anomaly 322 | $m = 359.2242 + 29.10535608 * $k - 0.0000333 * $t2 - 0.00000347 * $t3; 323 | 324 | // Moon's mean anomaly 325 | $mprime = 306.0253 + 385.81691806 * $k + 0.0107306 * $t2 + 0.00001236 * $t3; 326 | 327 | // Moon's argument of latitude 328 | $f = 21.2964 + 390.67050646 * $k - 0.0016528 * $t2 - 0.00000239 * $t3; 329 | 330 | if ($phase < 0.01 || abs($phase - 0.5) < 0.01) { 331 | // Corrections for New and Full Moon 332 | $pt += (0.1734 - 0.000393 * $t) * sin(deg2rad($m)) 333 | + 0.0021 * sin(deg2rad(2 * $m)) 334 | - 0.4068 * sin(deg2rad($mprime)) 335 | + 0.0161 * sin(deg2rad(2 * $mprime)) 336 | - 0.0004 * sin(deg2rad(3 * $mprime)) 337 | + 0.0104 * sin(deg2rad(2 * $f)) 338 | - 0.0051 * sin(deg2rad($m + $mprime)) 339 | - 0.0074 * sin(deg2rad($m - $mprime)) 340 | + 0.0004 * sin(deg2rad(2 * $f + $m)) 341 | - 0.0004 * sin(deg2rad(2 * $f - $m)) 342 | - 0.0006 * sin(deg2rad(2 * $f + $mprime)) 343 | + 0.0010 * sin(deg2rad(2 * $f - $mprime)) 344 | + 0.0005 * sin(deg2rad($m + 2 * $mprime)) 345 | ; 346 | 347 | $apcor = true; 348 | } elseif (abs($phase - 0.25) < 0.01 || abs($phase - 0.75) < 0.01) { 349 | $pt += (0.1721 - 0.0004 * $t) * sin(deg2rad($m)) 350 | + 0.0021 * sin(deg2rad(2 * $m)) 351 | - 0.6280 * sin(deg2rad($mprime)) 352 | + 0.0089 * sin(deg2rad(2 * $mprime)) 353 | - 0.0004 * sin(deg2rad(3 * $mprime)) 354 | + 0.0079 * sin(deg2rad(2 * $f)) 355 | - 0.0119 * sin(deg2rad($m + $mprime)) 356 | - 0.0047 * sin(deg2rad($m - $mprime)) 357 | + 0.0003 * sin(deg2rad(2 * $f + $m)) 358 | - 0.0004 * sin(deg2rad(2 * $f - $m)) 359 | - 0.0006 * sin(deg2rad(2 * $f + $mprime)) 360 | + 0.0021 * sin(deg2rad(2 * $f - $mprime)) 361 | + 0.0003 * sin(deg2rad($m + 2 * $mprime)) 362 | + 0.0004 * sin(deg2rad($m - 2 * $mprime)) 363 | - 0.0003 * sin(deg2rad(2 * $m + $mprime)) 364 | ; 365 | 366 | // First and last quarter corrections 367 | if ($phase < 0.5) { 368 | $pt += 0.0028 - 0.0004 * cos(deg2rad($m)) + 0.0003 * cos(deg2rad($mprime)); 369 | } else { 370 | $pt += -0.0028 + 0.0004 * cos(deg2rad($m)) - 0.0003 * cos(deg2rad($mprime)); 371 | } 372 | 373 | $apcor = true; 374 | } 375 | 376 | return $apcor ? $pt : null; 377 | } 378 | 379 | /** 380 | * Find time of phases of the moon which surround the current date. Five phases are found, starting and 381 | * ending with the new moons which bound the current lunation. 382 | */ 383 | protected function phaseHunt(): void 384 | { 385 | $sdate = $this->getJulianFromUTC($this->timestamp); 386 | $adate = $sdate - 45; 387 | $ats = $this->timestamp - 86400 * 45; 388 | $yy = (int) gmdate('Y', $ats); 389 | $mm = (int) gmdate('n', $ats); 390 | 391 | $k1 = floor(($yy + (($mm - 1) * (1 / 12)) - 1900) * 12.3685); 392 | $adate = $nt1 = $this->meanPhase((int) $adate, $k1); 393 | 394 | while (true) { 395 | $adate += $this->synmonth; 396 | $k2 = $k1 + 1; 397 | $nt2 = $this->meanPhase((int) $adate, $k2); 398 | 399 | // If nt2 is close to sdate, then mean phase isn't good enough, we have to be more accurate 400 | if (abs($nt2 - $sdate) < 0.75) { 401 | $nt2 = $this->truePhase($k2, 0.0); 402 | } 403 | 404 | if ($nt1 <= $sdate && $nt2 > $sdate) { 405 | break; 406 | } 407 | 408 | $nt1 = $nt2; 409 | $k1 = $k2; 410 | } 411 | 412 | // Results in Julian dates 413 | $dates = [ 414 | $this->truePhase($k1, 0.0), 415 | $this->truePhase($k1, 0.25), 416 | $this->truePhase($k1, 0.5), 417 | $this->truePhase($k1, 0.75), 418 | $this->truePhase($k2, 0.0), 419 | $this->truePhase($k2, 0.25), 420 | $this->truePhase($k2, 0.5), 421 | $this->truePhase($k2, 0.75), 422 | ]; 423 | 424 | $this->quarters = []; 425 | 426 | foreach ($dates as $jdate) { 427 | // Convert to UNIX time 428 | $this->quarters[] = ($jdate - 2_440_587.5) * 86400; 429 | } 430 | } 431 | 432 | /** 433 | * UTC to Julian 434 | */ 435 | protected function getJulianFromUTC(int $timestamp): float 436 | { 437 | return $timestamp / 86400 + 2_440_587.5; 438 | } 439 | 440 | /** 441 | * Returns the moon phase. 442 | */ 443 | public function getPhase(): float 444 | { 445 | return $this->phase; 446 | } 447 | 448 | public function getIllumination(): float 449 | { 450 | return $this->illumination; 451 | } 452 | 453 | public function getAge(): float 454 | { 455 | return $this->age; 456 | } 457 | 458 | public function getDistance(): float 459 | { 460 | return $this->distance; 461 | } 462 | 463 | public function getDiameter(): float 464 | { 465 | return $this->diameter; 466 | } 467 | 468 | public function getSunDistance(): float 469 | { 470 | return $this->sunDistance; 471 | } 472 | 473 | public function getSunDiameter(): float 474 | { 475 | return $this->sunDiameter; 476 | } 477 | 478 | /** 479 | * Get moon phase data 480 | */ 481 | public function getPhaseByName(string $name): ?float 482 | { 483 | $phases = [ 484 | 'new_moon', 485 | 'first_quarter', 486 | 'full_moon', 487 | 'last_quarter', 488 | 'next_new_moon', 489 | 'next_first_quarter', 490 | 'next_full_moon', 491 | 'next_last_quarter', 492 | ]; 493 | 494 | if (null === $this->quarters) { 495 | $this->phaseHunt(); 496 | } 497 | 498 | return $this->quarters[array_flip($phases)[$name]] ?? null; 499 | } 500 | 501 | /** 502 | * Get current phase name. There are eight phases, evenly split. 503 | * A "New Moon" occupies the 1/16th phases either side of phase = 0, and the rest follow from that. 504 | */ 505 | public function getPhaseName(): string 506 | { 507 | $names = [ 508 | 'New Moon', 509 | 'Waxing Crescent', 510 | 'First Quarter', 511 | 'Waxing Gibbous', 512 | 'Full Moon', 513 | 'Waning Gibbous', 514 | 'Third Quarter', 515 | 'Waning Crescent', 516 | 'New Moon', 517 | ]; 518 | 519 | return $names[floor(($this->phase + 0.0625) * 8)]; 520 | } 521 | 522 | public function getPhaseNewMoon(): ?float 523 | { 524 | return $this->getPhaseByName('new_moon'); 525 | } 526 | 527 | public function getPhaseFirstQuarter(): ?float 528 | { 529 | return $this->getPhaseByName('first_quarter'); 530 | } 531 | 532 | public function getPhaseFullMoon(): ?float 533 | { 534 | return $this->getPhaseByName('full_moon'); 535 | } 536 | 537 | public function getPhaseLastQuarter(): ?float 538 | { 539 | return $this->getPhaseByName('last_quarter'); 540 | } 541 | 542 | public function getPhaseNextNewMoon(): ?float 543 | { 544 | return $this->getPhaseByName('next_new_moon'); 545 | } 546 | 547 | public function getPhaseNextFirstQuarter(): ?float 548 | { 549 | return $this->getPhaseByName('next_first_quarter'); 550 | } 551 | 552 | public function getPhaseNextFullMoon(): ?float 553 | { 554 | return $this->getPhaseByName('next_full_moon'); 555 | } 556 | 557 | public function getPhaseNextLastQuarter(): ?float 558 | { 559 | return $this->getPhaseByName('next_last_quarter'); 560 | } 561 | } 562 | -------------------------------------------------------------------------------- /tests/MoonPhaseTest.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Tobias Köngeter 9 | * @copyright Copyright © Bit&Black 10 | * @link https://www.bitandblack.com 11 | * @license MIT 12 | */ 13 | 14 | namespace Solaris\Tests; 15 | 16 | use DateTime; 17 | use PHPUnit\Framework\TestCase; 18 | use Solaris\MoonPhase; 19 | 20 | class MoonPhaseTest extends TestCase 21 | { 22 | public function testPhaseName(): void 23 | { 24 | $dateTime = new DateTime('2021-01-01'); 25 | $moonPhase = new MoonPhase($dateTime); 26 | 27 | self::assertSame( 28 | 'Full Moon', 29 | $moonPhase->getPhaseName() 30 | ); 31 | } 32 | 33 | public function testGetNewMoon(): void 34 | { 35 | $dateTime = new DateTime('2021-01-01'); 36 | $moonPhase = new MoonPhase($dateTime); 37 | 38 | self::assertSame( 39 | 1_607_962_725.6397471, 40 | $moonPhase->getPhaseNewMoon() 41 | ); 42 | } 43 | } 44 | --------------------------------------------------------------------------------