Find when sunrise, sunset, and different twilights happen for a given location, based on the NOAA ESRL Solar Calculator.
Features high accuracy across several millenia, atmospheric refraction, a simple enumeration-based API, and multiple/missing events during polar night/day/twilight at extreme latitudes.
- .NET runtime that conforms to .NET Standard 2.0 or later
- .NET 5 or later
- .NET Core 2.0 or later
- .NET Framework 4.6.1 or later
The SolCalc
package is available on NuGet Gallery.
dotnet add package SolCalc
using NodaTime;
using NodaTime.Extensions;
using SolCalc;
using SolCalc.Data;
ZonedDateTime now = SystemClock.Instance.InZone(DateTimeZoneProviders.Tzdb["America/Los_Angeles"]).GetCurrentZonedDateTime();
const double lat = 37.35, lon = -121.95;
SunlightLevel currentSunlight = SunlightCalculator.GetSunlightAt(now, lat, lon);
SunlightChange nextSunrise = SunlightCalculator.GetSunlightChanges(now, lat, lon)
.First(change => change.Name == SolarTimeOfDay.Sunrise);
Console.WriteLine($"It is currently {currentSunlight} in Santa Clara, CA, US.");
Console.WriteLine($"The next sunrise will be {nextSunrise.Time:l<F> x}.");
// It is currently AstronomicalTwilight in Santa Clara, CA, US.
// The next sunrise will be Tuesday, April 23, 2024 6:23:01 am PDT.
For more information and diagrams, see "Twilight" on Wikipedia.
Quantized levels of sunlight brightness based on the sun's angle above the horizon at 0°. These represent durations from one solar time of day to the next. Each twilight generally occurs twice per day.
Level | Solar elevation range | Description |
---|---|---|
Daylight | [0°, 90°] | Sun is visible |
Civil twilight | [−6°, 0°) | Objects are visible |
Nautical twilight | [−12°, −6°) | Silhouettes are visible |
Astronomical twilight | [−18°, −12°) | Sunlight is only detectable with instruments |
Night | [−90°, −18°) | No sunlight |
Instants in a day when the sunlight level changes from one level to another.
Time of day | Sun direction | Previous light level | New light level | Solar elevation |
---|---|---|---|---|
Astronomical dawn | Rising | Night | Astronomical twilight | −18° |
Nautical dawn | Rising | Astronomical twilight | Nautical twilight | −12° |
Civil dawn | Rising | Nautical twilight | Civil twilight | −6° |
Sunrise | Rising | Civil twilight | Daylight | 0° |
Sunset | Setting | Daylight | Civil twilight | 0° |
Civil dusk | Setting | Civil twilight | Nautical twilight | −6° |
Nautical dusk | Setting | Nautical twilight | Astronomical twilight | −12° |
Astronomical dusk | Setting | Astronomical twilight | Night | −18° |
The main entry point into this library is the SolCalc.SunlightCalculator
static class.
Use SunlightCalculator.GetSunlightAt(ZonedDateTime, double, double)
to return the sunlight level at the given geographic coordinates at the given instant.
The instant's time zone must be the same zone that the given location observes, otherwise the result will be wrong. In the example below, Santa Clara, CA is in the America/Los_Angeles
time zone.
using NodaTime;
using NodaTime.Extensions;
using SolCalc;
using SolCalc.Data;
DateTimeZone zone = DateTimeZoneProviders.Tzdb["America/Los_Angeles"];
ZonedDateTime now = SystemClock.Instance.InZone(zone).GetCurrentZonedDateTime();
SunlightLevel level = SunlightCalculator.GetSunlightAt(now, latitude: 37.35, longitude: -121.95);
Console.WriteLine($"It is currently {level} in Santa Clara, CA, US.");
// It is currently Night in Santa Clara, CA, US.
Use SunlightCalculator.GetSunlightChanges(ZonedDateTime, double, double)
to get an infinite series of all future sunlight level changes at a particular location, starting after a given time.
The instant's time zone must be the same zone that the given location observes, otherwise the result will be wrong. In the example below, Santa Clara, CA is in the America/Los_Angeles
time zone.
The returned IEnumerable<SunlightChange>
is infinitely long because the sun will always rise again. It is not bounded by the end of the day. This means you should not try to call .ToList()
, .Count()
, or any other method that fully enumerates it, because they will never end. Instead, use filtering to get just the items you want, using methods like .TakeWhile()
, .SkipWhile()
, .Where()
, and .First()
.
DateTimeZone zone = DateTimeZoneProviders.Tzdb["America/Los_Angeles"];
ZonedDateTime now = SystemClock.Instance.InZone(zone).GetCurrentZonedDateTime();
SunlightChange nextChange = SunlightCalculator.GetSunlightChanges(now, latitude: 37.35, longitude: -121.95).First();
Console.WriteLine($"At {nextChange.Time}, {nextChange.Name} will start when {nextChange.PreviousSunlightLevel} changes to {nextChange.NewSunlightLevel} in Santa Clara, CA, US.");
// At 2024-04-22T04:48:01 America/Los_Angeles (-07), AstronomicalDawn will start when Night changes to AstronomicalTwilight in Santa Clara, CA, US.
DateTimeZone zone = DateTimeZoneProviders.Tzdb["America/Los_Angeles"];
ZonedDateTime now = SystemClock.Instance.InZone(zone).GetCurrentZonedDateTime();
IEnumerable<SunlightChange> dailySunriseAndSunset = SunlightCalculator.GetSunlightChanges(now, 37.35, -121.95)
.TakeWhile(s => s.Time.Date == now.Date).ToList();
ZonedDateTime? sunrise = dailySunriseAndSunset.FirstOrNull(s => s.Name == SolarTimeOfDay.Sunrise)?.Time;
ZonedDateTime? sunset = dailySunriseAndSunset.FirstOrNull(s => s.Name == SolarTimeOfDay.Sunset)?.Time;
DateTimeZone zone = DateTimeZoneProviders.Tzdb["America/Los_Angeles"];
ZonedClock clock = SystemClock.Instance.InZone(zone);
IEnumerable<SunlightChange> sunlightChanges = SunlightCalculator.GetSunlightChanges(clock.GetCurrentZonedDateTime(), 37.35, -121.95);
foreach (SunlightChange sunlightChange in sunlightChanges) {
Duration delay = sunlightChange.Time.Minus(clock.GetCurrentZonedDateTime());
Console.WriteLine($"Waiting {delay} for {sunlightChange.Name} at {sunlightChange.Time}");
await Task.Delay(delay.ToTimeSpan());
Console.WriteLine($"It is currently {sunlightChange.Name} at {clock.GetCurrentZonedDateTime()} in Santa Clara, CA, US");
}
This library uses the solar position calculation algorithm from the National Oceanic and Atmospheric Administration's Earth System Research Laboratories' Global Monitoring Laboratory's web-based Solar Calculator.
Warning
For research and recreational use only. Not for legal use.
If you want to calculate solar position or solar noon, you can use the SolarCalculator
static class.
- Time accuracy decreases from ≤ ±1 minute at latitudes ≤ ±72°, to ≤ ±10 minutes at latitudes > ±72°.
- Atmospheric refraction is taken in account.
- Clouds, air pressure, humidity, dust, other atmospheric conditions, observer's altitude, and solar eclipses are not taken into account.
- Years between −2000 and 3000 can be handled.
- Dates before October 15, 1582 might not use the correct calendar system.
- Years between 1800 and 2100 have the highest accuracy results. Years between −1000 and 3000 have medium accuracy results. Years outside those ranges have lower accuracy results.