Space: the final frontier. Or at least a topic all developers can enjoy. The most prominent heavenly body in the night sky is the Earth’s Moon, the only natural satellite the Earth has. The moon does many things; amongst them is help stabilize our planet’s wobble and, in turn, regulate our climate. Oh, moon, you’re so special to all of us, and that’s why I wanted to learn what phase you’re currently going through.

For those unfamiliar with the Moon phases, there are eight known phases: new moon, waxing crescent, first quarter, waxing gibbous, full moon, waning gibbous, third quarter, and waning crescent. This cycle repeats every 29.53 days and is unrelated to our calendars. Additionally, the way the face of the moon can look depends on whether we’re standing in the Northern or Southern hemisphere of the Earth, having a reversed look.

In this post, we’ll write a Moon class, that will help us calculate the current phase of the moon, and approximately how many days we are into the phase cycle.

The Moon Calculation

Before we start looking at the code to calculate the moon phase, we need to understand the basics of the calculation.

Step #1 requires we know the date of the last new moon. Luckily, many online resources can help us do just that. In this case, I used TimeAndDate.com to determine a new moon was over London, England, on January 21st, 1920, at 5:25 AM. We could calculate back further, but time is a human construct, and dates and times start to lose fidelity the further back we go.

Step #2 requires we convert all dates to Julian time. That includes the date and time from above, and the current date and time. In C#, that’s quite easy to do with the following code. Thanks to this little snippet and its author.

const double julianConstant = 2415018.5;
var julianDate = utcDateTime.ToOADate() + julianConstant;

// London New Moon (1920)
// https://www.timeanddate.com/moon/phases/uk/london?year=1920
var daysSinceLastNewMoon =
    new DateTime(1920, 1, 21, 5, 25, 00, DateTimeKind.Utc).ToOADate() + julianConstant;

As mentioned in the introduction, we know the Moon cycles every 29.53 days. Working from the last new moon, we can count to the current date and determine the days into the current cycle we are. Step #3 is just math.

var newMoons = 
    (julianDate - daysSinceLastNewMoon) / TotalLengthOfCycle;

var intoCycle = 
    (newMoons - Math.Truncate(newMoons)) * TotalLengthOfCycle;

After we check our math, we can use the resulting value to determine many things about the moon phase: the way the moon looks to our eye, what percentage of the moon is visible, and the name you might use to describe the moon.

The Moon API

Once we understand the calculations necessary to understand the moon’s phase, we can start building our surface API. I opted for a static class since we only have one moon, and this class will not refer to other planets’ moons.

var result = Moon.Now();
Console.WriteLine(result);

Now refers to the moment on the machine. It is converted to UTC to match the location and time of our initial piece of data. Our result is a PhaseResult that tells us about a few properties of the moon’s phase.

public string Name { get; }
public string Emoji { get; set; }
public double DaysIntoCycle { get; set; }
public Earth.Hemispheres Hemisphere { get; set; }
public DateTime Moment { get; }
public double Visibility  {get;}

We also want to be able to determine any date’s moon phase. So we also have a Calculate method.

var dates = new[] {
    new DateTime(2020, 7, 27), new DateTime(2020, 7, 28),
    new DateTime(2020, 8, 3), new DateTime(2020, 8, 11),
    new DateTime(2020, 8, 19),
};

var results =
    dates
        .Select(x => Moon.Calculate(x))
        .Select((r, i) => $"{r.Emoji} {r.Name} ({r.DaysIntoCycle} days, Visibility {Math.Round(r.Visibility, 2)}%)\n")
        .Aggregate((a, v) => a + v);

Running this code, we get the following results.

🌗 First Quarter (6.98 days, Visibility 47.27%)
🌖 Waxing Gibbous (7.98 days, Visibility 54.05%)
🌕 Full moon (13.98 days, Visibility 94.68%)
🌓 Third Quarter (21.98 days, Visibility 51.13%)
🌑 New Moon (0.45 days, Visibility 3.05%)

Awesome right?!

The Code

I’ve added the project to my GitHub repository, and feel free to clone, fix, and submit pull requests. For those who want to see the code for the Moon, here it is.

using System;
using System.Collections.Generic;
using System.Linq;

namespace MoonPhaseConsole
{
    public static class Moon
    {
        private static readonly IReadOnlyList<string> NorthernHemisphere
            = new List<string> {"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑"};

        private static readonly IReadOnlyList<string> SouthernHemisphere
            = NorthernHemisphere.Reverse().ToList();

        private static readonly List<string> Names = new List<string>
        {
            Phase.NewMoon,
            Phase.WaxingCrescent, Phase.FirstQuarter, Phase.WaxingGibbous,
            Phase.FullMoon,
            Phase.WaningGibbous, Phase.ThirdQuarter, Phase.WaningCrescent
        };

        private const double TotalLengthOfCycle = 29.53;
        
        private static readonly List<Phase> allPhases = new List<Phase>();

        static Moon()
        {
            var period = TotalLengthOfCycle / Names.Count;
            // divide the phases into equal parts 
            // making sure there are no gaps
            allPhases = Names
                .Select((t, i) => new Phase(t, period * i, period * (i + 1)))
                .ToList();
        }

        /// <summary>
        /// Calculate the current phase of the moon.
        /// Note: this calculation uses the last recorded new moon to calculate the cycles of
        /// of the moon since then. Any date in the past before 1920 might not work.
        /// </summary>
        /// <param name="utcDateTime"></param>
        /// <remarks>https://www.subsystems.us/uploads/9/8/9/4/98948044/moonphase.pdf</remarks>
        /// <returns></returns>
        public static PhaseResult Calculate(DateTime utcDateTime,
            Earth.Hemispheres viewFromEarth = Earth.Hemispheres.Northern)
        {
            const double julianConstant = 2415018.5;
            var julianDate = utcDateTime.ToOADate() + julianConstant;

            // London New Moon (1920)
            // https://www.timeanddate.com/moon/phases/uk/london?year=1920
            var daysSinceLastNewMoon =
                new DateTime(1920, 1, 21, 5, 25, 00, DateTimeKind.Utc).ToOADate() + julianConstant;

            var newMoons = (julianDate - daysSinceLastNewMoon) / TotalLengthOfCycle;
            var intoCycle = (newMoons - Math.Truncate(newMoons)) * TotalLengthOfCycle;

            var phase =
                allPhases.First(p => intoCycle >= p.Start && intoCycle <= p.End);

            var index = allPhases.IndexOf(phase);
            var currentPhase =
                viewFromEarth switch
                {
                    Earth.Hemispheres.Northern => NorthernHemisphere[index],
                    _ => SouthernHemisphere[index]
                };

            return new PhaseResult
            (
                phase.Name,
                currentPhase,
                Math.Round(intoCycle, 2),
                viewFromEarth,
                utcDateTime
            );
        }

        public static PhaseResult UtcNow(Earth.Hemispheres viewFromEarth = Earth.Hemispheres.Northern)
        {
            return Calculate(DateTime.UtcNow, viewFromEarth);
        }

        public static PhaseResult Now(Earth.Hemispheres viewFromEarth = Earth.Hemispheres.Northern)
        {
            return Calculate(DateTime.Now.ToUniversalTime(), viewFromEarth);
        }

        public class PhaseResult
        {
            public PhaseResult(string name, string emoji, double daysIntoCycle, Earth.Hemispheres hemisphere,
                DateTime moment)
            {
                Name = name;
                Emoji = emoji;
                DaysIntoCycle = daysIntoCycle;
                Hemisphere = hemisphere;
                Moment = moment;
            }

            public string Name { get; }
            public string Emoji { get; set; }
            public double DaysIntoCycle { get; set; }
            public Earth.Hemispheres Hemisphere { get; set; }
            public DateTime Moment { get; }
            public double Visibility  
            {
                get
                {
                    const int FullMoon = 15;
                    const double halfCycle = TotalLengthOfCycle / 2;
                
                    var numerator = DaysIntoCycle > FullMoon
                        // past the full moon, we want to count down
                        ? halfCycle - (DaysIntoCycle % halfCycle)
                        // leading up to the full moon
                        : DaysIntoCycle;

                    return numerator / halfCycle * 100;
                }
            }

            public override string ToString()
            {
                var percent = Math.Round(Visibility , 2);
                return $"The Moon for {Moment} is {DaysIntoCycle} days\n" +
                       $"into the cycle, and is showing as \"{Name}\"\n" +
                       $"with {percent}% visibility, and a face of {Emoji} from the {Hemisphere.ToString().ToLowerInvariant()} hemisphere.";
            }
        }

        public class Phase
        {
            public const string NewMoon = "New Moon";
            public const string WaxingCrescent = "Waxing Crescent";
            public const string FirstQuarter = "First Quarter";
            public const string WaxingGibbous = "Waxing Gibbous";
            public const string FullMoon = "Full Moon";
            public const string WaningGibbous = "Waning Gibbous";
            public const string ThirdQuarter = "Third Quarter";
            public const string WaningCrescent = "Waning Crescent";

            public Phase(string name, double start, double end)
            {
                Name = name;
                Start = start;
                End = end;
            }

            public string Name { get; }

            /// <summary>
            /// The days into the cycle this phase starts
            /// </summary>
            public double Start { get; }

            /// <summary>
            /// The days into the cycle this phase ends
            /// </summary>
            public double End { get; }
        }
    }
}

Conclusion

The moon makes me happy, and learning more about it has been a fun experience. While the calculations are not perfect, they are relatively close and can make for a fun project for folks getting started with C#. For folks who need more accurate calculations, then I would suggest using a web API.

My plans for this code is to ultimately build a fun Xamarin mobile application with a date picker and emoji output. Other than that, I have no real plans for this library.

I am surprised that the .NET Framework doesn’t have a first-class implementation for astronomy. It seems like a missed opportunity to add some fun parts to the framework. It’s not like the universe is going away any time soon.

If you liked this post, please leave a comment and share it with your friends.

If you want to learn more about the moon, NASA has a great overview.