Search code examples
phpalgorithmdatetimetimezonedate-arithmetic

Algorithm to calculate nightly hours of moonlight


I have been chewing on this problem for quite a while. I think I'm trying too hard and keep running around in circles.

The Problem

I need to calculate the hours of moonlight at night, i.e. the amount of time the moon is over the horizon outside the daylight hours. What is known are the sunrise/sunset and moonrise/moonset times in UTC for a given date. The easiest (hypthetical) scenario is the following:

sunrise:  06:45
sunset:   18:20
moonrise: 02:30
moonset:  19:50

The algorithm to calculate moonlight hours would be:

if(moonrise<sunset && sunrise<moonset) {
   moonlighthours = (sunrise-moonrise)-(moonset-sunset);
}

Equally easy:

sunrise:  06:45
sunset:   18:20
moonrise: 10:30
moonset:  19:50

if(moonrise>sunset && sunrise<moonset) {
   moonlighthours = (moonset-sunset);
}

But as we're dealing with UTC it can get quite complicated depending on the time zone as sunrise/set and moonrise/set times might be stretched across three different dates:

sunrise:  2014-02-05 23:30 
sunset:   2014-02-06 12:20
moonrise: 2014-02-06 11:00
moonset:  2014-03-07 00:50

So to calculate the moonlight hours for 2014-02-06 I would try something convoluted like this:

if(sunrise<midnight)    { sunrise  = midnight;    }
if(sunset>midnight+24)  { sunset   = midnight+24; }
if(moonrise<midnight)   { moonrise = midnight;    }
if(moonset>midnight+24) { moonset  = midnight+24  }

if(moonrise<sunset && sunrise<moonset) {
   moonlighthours = (sunrise-moonrise)-(moonset-sunset);
} else if (moonrise>sunset && sunrise<moonset) {
   moonlighthours = (moonset-sunset);
} else if (moonrise<sunset && sunrise>moonset) {
   moonlighthours = (sunrise-moonrise);
} else {
   moonlighthours=0;
} 

Trying to cover all possible scenarios as a result of the phase shift between moonlight and daylight hours using if... else structures is a nightmare. So I am hoping that there is someone with a fresh take on the problem. Any help would be greatly appreciated.

EDIT

Here is the solution I have come up with based on @Ilmari Karonen's suggestions. This will caclulate the moonlight hours during a given calendar date (using PHP syntax):

$mid00  = midnight;          // unixtimestamp , e.g. 1391558400 (2014-02-05 00:00:00) 
$mid24  = midnight+86399;     // 1399075199 (2014-02-05 23:59:59)

$mr     = moonrise;
$ms     = moonset;
$sr     = sunrise;
$ss     = sunset;

$mr = $mr < $mid00 ? $mid00 : $mr;
$ms = $ms > $mid24 ? $mid24 : $ms;
$sr = $sr < $mid00 ? $mid00 : $sr;
$ss = $ss > $mid24 ? $Mid24 : $ss;

$ml_morn = 0;   // moonlight hours during morning night
$ml_even = 0;   // moonlight hours during evening night

if($ms > $mr) {                                      // moon set later than moon rise 
   $ml_morn = $mr < $sr ? $sr-$mr : 0;               // moon rises before sunrise?
   $ml_even = $ms > $ss ? $ms-$ss : 0;               // moon sets after sunset?
} else {                                             // moon set before moon rise
  $ml_even = $mr > $ss ? $mid24-$mr : $mid24 - $ss;  // moon rises before sunset?
  $ml_morn = $ms < $sr ? $ms-$mid00 : $sr - $mid00;  // moon sets before sunrise? 
}

moonlight_hours = $ml_morn = $ml_even;

Solution

  • When you have a complicated problem, it's often useful to break it up into simpler steps:

    Step 1: If necessary, convert the sun/moonrise/set times into proper datetime values that you can do arithmetic with. Second since the Unix epoch (or minutes since the start of the millennium, or whatever) will do, but if your language has an appropriate datetime class or type, I'd recommend using that.

    Since this conversion is, presumably, the same for all the timestamps, you can write a single function that will do it for an arbitrary time value, and call it for each of your inputs.

    Step 2: Find the start and end times of the moonlight period(s).

    If you want the moonlight hours for one night, then you can calculate the start and end of the moonlight period as:

    start = max(moonrise, sunset)
    end   = min(sunrise, moonset)
    

    If you really need the moonlight hours for one calendar day, it's a bit more complicated, since there may be two separate moonlight periods. You can calculate their respective start and end times as:

    start1 = max(moonrise1, sunset1, midnight)
    end1   = min(sunrise1, moonset1)
    start2 = max(moonrise2, sunset2)
    end1   = min(sunrise2, moonset2, midnight + 24 hours)
    

    where the variable names ending in 1 denote the respective times for the night before the given day, and the variable names ending in 2 denote the respective times for the night after the given day.

    Step 3: Calculate the length(s) of the moonlight period(s).

    This is just simple datetime subtraction. Note that the subtraction might give a negative value, if the moon is not visible at all on a given night, in which case you should clamp the result to zero.

    If you're doing the calculation for a specific calendar date, you should repeat this for both the previous and the next night, and then add the (non-negative) results together.