Command-line hacking: calculating the phase of the Moon

display This is another in my series of articles on doing off-beat and (I hope) interesting things with standard Linux command-line tools. In this post I'll demonstrate the GNU date utility, along with Bash shell arithmetic, to display the phase of the Moon -- today, or on some selected day. This example is simple enough that most of the code is in the article itself, but you can get the full script from my GitHb repository.

Here's an example of its use:

$ moonphase.sh "jan 28 2023"
Jan 28 2023: 19%, first quarter

For convenience, if you don't supply a date argument, it will display the Moon phase today.

So how do we work out the Moon phase? It's easy enough if we keep in mind that the synodic month -- the time between corresponding Moon phases -- is reasonably constant over time. It does vary a little, because the Moon's orbit is subject to gravitational disturbances from other celestial bodies; but these variations do not accumulate -- not over practical timespans, anyway. The average synodic month is just over 29.5 Earth days -- the figure I use in the program is in seconds:

SYNODIC_SECONDS=2551443

Over the synodic month, the Moon phase varies from new to full and back again. We can assign a percentage to the fraction of the synodic month that has passed: 0% and 100% are both new moons, with full moon at 50%. It's more elegant to use a fraction 0-1.0 for this, but Bash can't do floating-point math. So, instead, I scale all the numbers by a factor of 100 and, in effect, work in percentages.

Note:
Some published tables display a percentage of the Moon's total illumination -- 0% at new and 100% at full. This figure is related to the Moon phase, but in a rather complex way. It's the fractional phase we care about here, not the illumination.

If it's new moon (0%) now, in about 14.75 days, or about 1274400 seconds, it will be full moon (50%).

To do the calculation, we need some kind of fixed reference point. That is, we need to specify a date on which we know the Moon phase exactly. From that point, we can calculate the number of synodic months that have elapsed, and thus how far into the Moon's cycle we are. It doesn't matter hugely what the reference date is. In the program I have:

REF_DATE="Jan 15 2023"
REF_PERCENT=75

That is, the Moon was 75% of the way through its cycle (that's "third quarter") on Jan 15, 2023. I have to stress that it really doesn't matter what date you choose (within reason) so long as you can look up the exact moon phase on that day. Frankly, you could just wait until the Moon is next full, and note the date -- that's sufficient accuracy for this application. However, the whole calculation hinges on having a single, reasonably accurate reference point.

The first calculation will be to determine the time difference in seconds between the time of interest and the reference time. That's easy, because the date utility can display a time in seconds since the Unix epoch. It doesn't matter what that 'epoch' time actually is -- we're only interested in a time difference here.

REF_EPOCH=`date --date "$REF_DATE" +%s`
NOW_EPOCH=`date +%s --date .... 
SEC_DIFF=$(($NOW_EPOCH - $REF_EPOCH))

It's the '+%s' that gives the result in seconds. To do the subtraction, I'm using the Bash arithmetic construct $((...)). A more traditional formulation, using expr might be:

SEC_DIFF=`expr $NOW_EPOCH - $REF_EPOCH`
Note:
SEC_DIFF could be positive or negative, depending on whether the date of interest is before or after the reference date. We'll take care of that later.

Now we need to convert this time in seconds to a number of synodic months:

CENTI_MOONS=$((100 * $SEC_DIFF / $SYNODIC_SECONDS))

Actually, I'm getting the result in hundredths of a synodic month, which I call 'centi-moons'. That's because we're working with Moon phase as a percentage, and because Bash only supports integer arithmetic. Incidentally, as a matter of strict math there's no difference between these two expressions:

centi_moons = 100 * sec_diff / synodic_seconds
centi_moons = sec_diff / synodic_seconds * 100

But there's all the difference in the world when we're working with integer arithmetic: the division term on its own would evaluate to zero.

To complete the calculation we add the number of centi-moons to the reference moon phase (which is a percentage). Because the result might be greater than 100%, we use the modulo '%' operator to reduce the result to the 0-100% range, and ensure it is positive.

MOON_PERCENT=$((($CENTI_MOONS + $REF_PERCENT) % 100))

So we now have the Moon phase as a percentage, with new moon at 0% and full moon at 50%. To display the name of the phase, we can use Bash integer comparison operations.

if [[ $MOON_PERCENT -lt 7 ]] ; then
  echo "new"
elif [[ $MOON_PERCENT -lt 19 ]] ; then
  echo "waxing crescent"
#... and so on

In the interests of full disclosure I should point out that the true figures that correspond to to '7' and '19' in the snippet above are '6.25' and '18.75'. Remember that there are 8 Moon phases, and 100/8 is not a whole number. But these rounded figures are close enough for our purposes, and avoid having to use other tricks to work around the absence of floating-point math support in Bash.

So that's it -- how to work out the Moon phase in a Bash script. An exercise for the reader is to work out moonrise and moonset times, using a similar technique.