Skip to content

Instantly share code, notes, and snippets.

@RichardD2
Last active January 7, 2020 09:49
Show Gist options
  • Save RichardD2/f6b08a5791b21ac77ce7 to your computer and use it in GitHub Desktop.
Save RichardD2/f6b08a5791b21ac77ce7 to your computer and use it in GitHub Desktop.
Sunrise / Sunset calculation
using System;
using System.Diagnostics;
namespace Trinet.Core
{
/// <summary>
/// Represents the time of sunrise and sunset for a date and location.
/// </summary>
/// <remarks>
/// <para>The calculated times have a precision of approximately three mintes.
/// Other factors, such as air temperature and altitude, may affect the times by up to five minutes.</para>
///
/// <para>At very high/low latitudes (close to the poles), there are days when the sun never rises (during the winter)
/// or never sets (during the summer). In these cases, the <see cref="SunriseTimeUtc"/> and
/// <see cref="SunsetTimeUtc"/> properties will return <see langword="null"/>.</para>
///
/// <para>The calculation was found at: <a href="http://users.electromagnetic.net/bu/astro/sunrise-set.php" target="_blank">users.electromagnetic.net/bu/astro/sunrise-set.php</a>.</para>
/// <para>The constants are defined at: <a href="http://www.astro.uu.nl/~strous/AA/en/reken/zonpositie.html" target="_blank">www.astro.uu.nl/~strous/AA/en/reken/zonpositie.html</a></para>
/// <para>See also: <a href="http://en.wikipedia.org/wiki/Sunrise_equation" target="_blank">en.wikipedia.org/wiki/Sunrise_equation</a></para>
/// </remarks>
/// <example>
/// <code>
/// var location = GeographicLocation.Parse("50° 49\' 55.8717\" N, 0° 17\' 45.2833\" E");
/// var hours = DaylightHours.Calculate(2010, 1, 8, location);
/// Console.WriteLine("On {0:D}, sun rises at {1:hh:mm tt} and sets at {2:hh:mm tt}", hours.Day, hours.SunriseUtc, hours.SunsetUtc);
/// // Output: "On 08 January 2010, sun rises at 08:00 AM and sets at 04:12 PM"
/// </code>
/// </example>
public sealed class DaylightHours
{
private static readonly DateTimeOffset BaseJulianDate = new DateTimeOffset(2000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);
private const double BaseJulianDays = 2451545;
private const double EarthDegreesPerYear = 357.5291;
private const double EarthDegreesPerDay = 0.98560028;
private const double EarthEocCoeff1 = 1.9148;
private const double EarthEocCoeff2 = 0.0200;
private const double EarthEocCoeff3 = 0.0003;
private const double EarthPerihelion = 102.9372;
private const double EarthObliquity = 23.45;
private const double EarthTransitJ1 = 0.0053;
private const double EarthTransitJ2 = -0.0069;
private const double EarthSolarDiskDiameter = -0.83;
private const double Epsilon = 0.000001;
private readonly GeographicLocation _location;
private readonly DateTimeOffset _day;
private readonly TimeSpan? _sunrise;
private readonly TimeSpan? _sunset;
/// <summary>
/// Initializes a new instance of the <see cref="DaylightHours"/> class.
/// </summary>
/// <param name="location">
/// The location.
/// </param>
/// <param name="day">
/// The day.
/// </param>
/// <param name="sunrise">
/// The sunrise time.
/// </param>
/// <param name="sunset">
/// The sunset time.
/// </param>
private DaylightHours(GeographicLocation location, DateTimeOffset day, TimeSpan? sunrise, TimeSpan? sunset)
{
_day = day;
_location = location;
_sunrise = sunrise;
_sunset = sunset;
}
/// <summary>
/// Returns the geographic location for which these times were calculated.
/// </summary>
/// <value>
/// The <see cref="GeographicLocation"/> for which these times were calculated.
/// </value>
public GeographicLocation Location
{
get { return _location; }
}
/// <summary>
/// Returns the day for which these times were calculated.
/// </summary>
/// <value>
/// The day for which these times were calculated.
/// </value>
public DateTimeOffset Day
{
get { return _day; }
}
/// <summary>
/// Returns the sunrise time in UTC, if any.
/// </summary>
/// <value>
/// The sunrise time in UTC, if any.
/// </value>
public TimeSpan? SunriseTimeUtc
{
get { return _sunrise; }
}
/// <summary>
/// Returns the sunrise date and time in UTC, if any.
/// </summary>
/// <value>
/// The sunrise date and time in UTC, if any.
/// </value>
public DateTimeOffset? SunriseUtc
{
get
{
if (_sunrise == null) return null;
return _day + _sunrise.Value;
}
}
/// <summary>
/// Returns the sunset time in UTC, if any.
/// </summary>
/// <value>
/// The sunset time in UTC, if any.
/// </value>
public TimeSpan? SunsetTimeUtc
{
get { return _sunset; }
}
/// <summary>
/// Returns the sunset date and time in UTC, if any.
/// </summary>
/// <value>
/// The sunset date and time in UTC, if any.
/// </value>
public DateTimeOffset? SunsetUtc
{
get
{
if (_sunset == null) return null;
return _day + _sunset.Value;
}
}
private static double DegreesToRadians(double degrees)
{
return (degrees / 180D) * Math.PI;
}
private static double RadiansToDegrees(double radians)
{
return (radians / Math.PI) * 180D;
}
private static double Sin(double degrees)
{
return Math.Sin(DegreesToRadians(degrees));
}
private static double Asin(double d)
{
return RadiansToDegrees(Math.Asin(d));
}
private static double Cos(double degrees)
{
return Math.Cos(DegreesToRadians(degrees));
}
private static double Acos(double d)
{
return RadiansToDegrees(Math.Acos(d));
}
private static double ToJulian(DateTimeOffset value)
{
return BaseJulianDays + (value - BaseJulianDate).TotalDays;
}
private static DateTimeOffset FromJulian(double value)
{
return BaseJulianDate.AddDays(value - BaseJulianDays).AddHours(12);
}
private static DaylightHours CalculateCore(DateTimeOffset day, GeographicLocation location)
{
double jdate = ToJulian(day);
double n = Math.Round((jdate - BaseJulianDays - 0.0009D) - (location.LongitudeWest / 360D));
double jnoon = BaseJulianDays + 0.0009D + (location.LongitudeWest / 360D) + n;
double m = (EarthDegreesPerYear + (EarthDegreesPerDay * (jnoon - BaseJulianDays))) % 360;
double c = (EarthEocCoeff1 * Sin(m)) + (EarthEocCoeff2 * Sin(2 * m)) + (EarthEocCoeff3 * Sin(3 * m));
double sunLon = (m + EarthPerihelion + c + 180D) % 360;
double jtransit = jnoon + (EarthTransitJ1 * Sin(m)) + (EarthTransitJ2 * Sin(2 * sunLon));
int iteration = 0;
double oldMean;
do
{
oldMean = m;
m = (EarthDegreesPerYear + (EarthDegreesPerDay * (jtransit - BaseJulianDays))) % 360;
c = (EarthEocCoeff1 * Sin(m)) + (EarthEocCoeff2 * Sin(2 * m)) + (EarthEocCoeff3 * Sin(3 * m));
sunLon = (m + EarthPerihelion + c + 180D) % 360;
jtransit = jnoon + (EarthTransitJ1 * Sin(m)) + (EarthTransitJ2 * Sin(2 * sunLon));
}
while (iteration++ < 100 && Math.Abs(m - oldMean) > Epsilon);
double sunDec = Asin(Sin(sunLon) * Sin(EarthObliquity));
double h = Acos((Sin(EarthSolarDiskDiameter) - Sin(location.Latitude) * Sin(sunDec)) / (Cos(location.Latitude) * Cos(sunDec)));
if (Math.Abs(h) < Epsilon || double.IsNaN(h) || double.IsInfinity(h))
{
return new DaylightHours(location, day, null, null);
}
double jnoon2 = BaseJulianDays + 0.0009D + ((h + location.LongitudeWest) / 360D) + n;
double jset = jnoon2 + (EarthTransitJ1 * Sin(m)) + (EarthTransitJ2 * Sin(2 * sunLon));
double jrise = jtransit - (jset - jtransit);
DateTimeOffset sunrise = FromJulian(jrise);
Debug.Assert(sunrise.Date == day.Date, "Wrong sunrise", "Sunrise: Expected {0:D} but got {1:D} instead.", day, sunrise.Date);
DateTimeOffset sunset = FromJulian(jset);
Debug.Assert(sunset.Date == day.Date, "Wrong sunset", "Sunset: Expected {0:D} but got {1:D} instead.", day, sunset.Date);
return new DaylightHours(location, day, sunrise.TimeOfDay, sunset.TimeOfDay);
}
/// <summary>
/// Calculates the sunrise and sunset days for the specified date and location.
/// </summary>
/// <param name="day">
/// The date for which the times are calculated.
/// </param>
/// <param name="location">
/// The <see cref="GeographicLocation"/> for which the times are calculated.
/// </param>
/// <returns>
/// A <see cref="DaylightHours"/> instance representing the calculated times.
/// This value will never be <see langword="null"/>, although the sunrise/sunset times could be.
/// </returns>
/// <exception cref="InvalidOperationException">
/// There was an error with the calculation.
/// </exception>
public static DaylightHours Calculate(DateTimeOffset day, GeographicLocation location)
{
return CalculateCore(day.UtcDateTime.Date, location);
}
/// <summary>
/// Calculates the sunrise and sunset days for the specified date and location.
/// </summary>
/// <param name="day">
/// The date for which the times are calculated.
/// </param>
/// <param name="location">
/// The <see cref="GeographicLocation"/> for which the times are calculated.
/// </param>
/// <returns>
/// A <see cref="DaylightHours"/> instance representing the calculated times.
/// This value will never be <see langword="null"/>, although the sunrise/sunset times could be.
/// </returns>
/// <exception cref="InvalidOperationException">
/// There was an error with the calculation.
/// </exception>
public static DaylightHours Calculate(DateTime day, GeographicLocation location)
{
return CalculateCore(day.ToUniversalTime().Date, location);
}
/// <summary>
/// Calculates the sunrise and sunset days for the specified date and location.
/// </summary>
/// <param name="year">
/// The year for which the times are calculated.
/// </param>
/// <param name="month">
/// The month for which the times are calculated.
/// </param>
/// <param name="day">
/// The day for which the times are calculated.
/// </param>
/// <param name="location">
/// The <see cref="GeographicLocation"/> for which the times are calculated.
/// </param>
/// <returns>
/// A <see cref="DaylightHours"/> instance representing the calculated times.
/// This value will never be <see langword="null"/>, although the sunrise/sunset times could be.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <para><paramref name="year"/> is less than 1 or greater than 9999.</para>
/// <para>-or-</para>
/// <para><paramref name="month"/> is less than 1 or greater than 12.</para>
/// <para>-or-</para>
/// <para><paramref name="day"/> is less than 1 or greater than the number of days in the specified month.</para>
/// <para>-or-</para>
/// <para>The specified date is earlier than <see cref="DateTimeOffset.MinValue"/> or later than <see cref="DateTimeOffset.MaxValue"/>.</para>
/// </exception>
/// <exception cref="InvalidOperationException">
/// There was an error with the calculation.
/// </exception>
public static DaylightHours Calculate(int year, int month, int day, GeographicLocation location)
{
var date = new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero);
return CalculateCore(date, location);
}
}
}
using System;
using System.Globalization;
using System.Linq;
namespace Trinet.Core
{
/// <summary>
/// Represents a geographic location (latitude and longitude).
/// </summary>
public struct GeographicLocation : IEquatable<GeographicLocation>, IFormattable
{
private const double Epsilon = 0.000001;
private readonly double _latitude;
private readonly double _longitude;
/// <summary>
/// Initializes a new instance of the <see cref="GeographicLocation"/> struct.
/// </summary>
/// <param name="latitude">
/// The latitude.
/// </param>
/// <param name="longitude">
/// The longitude.
/// </param>
public GeographicLocation(double latitude, double longitude)
{
_latitude = ClampCoordinate(latitude);
_longitude = ClampCoordinate(longitude);
}
/// <summary>
/// Gets or sets the latitude of this location.
/// </summary>
/// <value>
/// The latitude of this location.
/// Positive values indicate North latitudes;
/// negative values indicate South latitudes.
/// </value>
public double Latitude
{
get { return _latitude; }
}
/// <summary>
/// Gets or sets the longitude of this location.
/// </summary>
/// <value>
/// The longitude of this location.
/// Positive values indicate East longitudes;
/// negative values indicate West longitudes.
/// </value>
public double Longitude
{
get { return _longitude; }
}
/// <summary>
/// Gets or sets the longitude-West of this location.
/// </summary>
/// <value>
/// The longitude-West of this location.
/// Positive values indicate West longitudes;
/// negative values indicate East longitudes.
/// </value>
internal double LongitudeWest
{
get { return -_longitude; }
}
private static string FormatCoordinate(double value, string suffixPositive, string suffixNegative, IFormatProvider provider, byte precision)
{
string suffix = (Math.Abs(value) < Epsilon) ? null : ((0D > value) ? suffixNegative : suffixPositive);
value = Math.Abs(value);
double degrees = Math.Truncate(value);
value -= degrees;
value *= 60;
double minutes = Math.Truncate(value);
value -= minutes;
value *= 60;
int partIndex = 0;
var parts = new string[4];
parts[partIndex++] = degrees.ToString("#,##0", provider) + "\u00b0";
if (Math.Abs(minutes) > Epsilon)
{
parts[partIndex++] = minutes.ToString("#,##0", provider) + "\'";
}
if (Math.Abs(value) > Epsilon)
{
string format = "###0." + ((0 >= precision) ? string.Empty : new string('0', precision));
parts[partIndex++] = value.ToString(format, provider) + "\"";
}
if (null != suffix)
{
parts[partIndex++] = suffix;
}
return string.Join(" ", parts, 0, partIndex);
}
/// <summary>
/// Returns a <see cref="string"/> that represents this instance.
/// </summary>
/// <param name="format">
/// A format string which controls how this instance is formatted.
/// Specify <see langword="null"/> to use the default format string (<c>G4</c>).
/// </param>
/// <param name="formatProvider">
/// The <see cref="IFormatProvider"/> instance which indicates how values are formatted.
/// Specify <see langword="null"/> to use the current thread's formatting info.
/// </param>
/// <returns>
/// A <see cref="string"/> that represents this instance.
/// </returns>
/// <remarks>
/// <para>Supported format strings are:</para>
/// <list type="table">
/// <item>
/// <term>R</term>
/// <description>
/// Round-trip format.
/// </description>
/// </item>
/// <item>
/// <term>F</term>
/// <description>
/// Fixed precision format.
/// Precision can be specified within the format string (eg: F6).
/// If not specified, the default precision is 6.
/// </description>
/// </item>
/// <item>
/// <term>E</term>
/// <description>
/// Scientific format.
/// Precision can be specified within the format string (eg: E6).
/// If not specified, the default precision is 6.
/// </description>
/// </item>
/// <item>
/// <term>G</term>
/// <description>
/// Geographic format (degrees, minutes and seconds).
/// Precision of the seconds can be specified within the format string (eg: G6).
/// If not specified, the default precision is 4.
/// </description>
/// </item>
/// </list>
/// <para>If any other format string is specified, it defaults to <c>G4</c>.</para>
/// </remarks>
public string ToString(string format, IFormatProvider formatProvider)
{
if (formatProvider == null) formatProvider = CultureInfo.CurrentCulture;
byte precision = 4;
if (!string.IsNullOrEmpty(format))
{
if ('F' == format[0] || 'f' == format[0])
{
if (1 == format.Length) format = "F6";
return Latitude.ToString(format, formatProvider)
+ ", " + Longitude.ToString(format, formatProvider);
}
if ('E' == format[0] || 'e' == format[0])
{
return Latitude.ToString(format, formatProvider)
+ ", " + Longitude.ToString(format, formatProvider);
}
if (string.Equals("R", format, StringComparison.OrdinalIgnoreCase))
{
return Latitude.ToString(format, formatProvider)
+ ", " + Longitude.ToString(format, formatProvider);
}
if ((format[0] == 'G' || format[0] == 'g') && format.Length != 1)
{
if (!byte.TryParse(format.Substring(1), NumberStyles.Integer, formatProvider, out precision))
{
precision = 4;
}
}
}
return FormatCoordinate(Latitude, "N", "S", formatProvider, precision)
+ ", " + FormatCoordinate(Longitude, "E", "W", formatProvider, precision);
}
/// <summary>
/// Returns a <see cref="string"/> that represents this instance.
/// </summary>
/// <param name="formatProvider">
/// The <see cref="IFormatProvider"/> instance which indicates how values are formatted.
/// Specify <see langword="null"/> to use the current thread's formatting info.
/// </param>
/// <returns>
/// A <see cref="string"/> that represents this instance,
/// represented as degrees, minutes and seconds, with 4 digits of precision for the seconds.
/// </returns>
public string ToString(IFormatProvider formatProvider)
{
return ToString(null, formatProvider);
}
/// <summary>
/// Returns a <see cref="string"/> that represents this instance.
/// </summary>
/// <param name="format">
/// A format string which controls how this instance is formatted.
/// Specify <see langword="null"/> to use the default format string (<c>G4</c>).
/// </param>
/// <returns>
/// A <see cref="string"/> that represents this instance.
/// </returns>
/// <remarks>
/// <para>Supported format strings are:</para>
/// <list type="table">
/// <item>
/// <term>R</term>
/// <description>
/// Round-trip format.
/// </description>
/// </item>
/// <item>
/// <term>F</term>
/// <description>
/// Fixed precision format.
/// Precision can be specified within the format string (eg: F6).
/// If not specified, the default precision is 6.
/// </description>
/// </item>
/// <item>
/// <term>E</term>
/// <description>
/// Scientific format.
/// Precision can be specified within the format string (eg: E6).
/// If not specified, the default precision is 6.
/// </description>
/// </item>
/// <item>
/// <term>G</term>
/// <description>
/// Geographic format (degrees, minutes and seconds).
/// Precision of the seconds can be specified within the format string (eg: G6).
/// If not specified, the default precision is 4.
/// </description>
/// </item>
/// </list>
/// <para>If any other format string is specified, it defaults to <c>G4</c>.</para>
/// </remarks>
public string ToString(string format)
{
return ToString(format, CultureInfo.CurrentCulture);
}
/// <summary>
/// Returns a <see cref="string"/> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="string"/> that represents this instance,
/// represented as degrees, minutes and seconds, with 4 digits of precision for the seconds.
/// </returns>
public override string ToString()
{
return ToString(null, CultureInfo.CurrentCulture);
}
private static double ClampCoordinate(double value)
{
value = value % 360;
if (value > 180D)
{
value -= 360D;
}
else if (value <= -180D)
{
value += 360D;
}
return value;
}
private static double? ParseCoordinate(string value, IFormatProvider provider)
{
double? result = null;
if (value.Length != 0)
{
string degrees = null, minutes = null, seconds = null;
var buffer = new char[value.Length];
int bufferIndex = 0;
foreach (char c in value)
{
switch (c)
{
case '\u00b0':
{
if (degrees != null) return null;
degrees = (bufferIndex == 0) ? string.Empty : new string(buffer, 0, bufferIndex);
bufferIndex = 0;
break;
}
case '\u0027':
case '\u02b9':
case '\u2032':
{
if (minutes != null) return null;
minutes = (bufferIndex == 0) ? string.Empty : new string(buffer, 0, bufferIndex);
bufferIndex = 0;
break;
}
case '\u0022':
case '\u02ba':
case '\u2033':
{
if (seconds != null) return null;
seconds = (bufferIndex == 0) ? string.Empty : new string(buffer, 0, bufferIndex);
bufferIndex = 0;
break;
}
case '.':
case '+':
case '-':
{
buffer[bufferIndex++] = c;
break;
}
case 'E':
case 'e':
{
buffer[bufferIndex++] = 'E';
break;
}
default:
{
if ('0' <= c && c <= '9')
{
buffer[bufferIndex++] = c;
}
else if (!char.IsWhiteSpace(c))
{
return null;
}
break;
}
}
}
if (0 != bufferIndex)
{
if (degrees == null)
{
degrees = new string(buffer, 0, bufferIndex);
}
else if (minutes == null)
{
minutes = new string(buffer, 0, bufferIndex);
}
else if (seconds == null)
{
seconds = new string(buffer, 0, bufferIndex);
}
}
if (!string.IsNullOrEmpty(degrees))
{
double d;
if (!double.TryParse(degrees, NumberStyles.Number | NumberStyles.AllowExponent, provider, out d))
{
return null;
}
result = d;
}
else
{
result = 0D;
}
if (!string.IsNullOrEmpty(minutes))
{
double m;
if (!double.TryParse(minutes, NumberStyles.Number | NumberStyles.AllowExponent, provider, out m))
{
return null;
}
result += m / 60D;
}
if (!string.IsNullOrEmpty(seconds))
{
double s;
if (!double.TryParse(seconds, NumberStyles.Number | NumberStyles.AllowExponent, provider, out s))
{
return null;
}
result += s / 3600D;
}
}
return result;
}
/// <summary>
/// Attempts to parse the string as a geographic location.
/// </summary>
/// <param name="value">
/// The string to parse.
/// </param>
/// <param name="culture">
/// A <see cref="CultureInfo"/> which determines how values are parsed.
/// </param>
/// <param name="result">
/// When this method returns, contains the parsed <see cref="GeographicLocation"/>, if available.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the value was parsed;
/// otherwise, <see langword="false"/>.
/// </returns>
public static bool TryParse(string value, CultureInfo culture, out GeographicLocation result)
{
if (string.IsNullOrWhiteSpace(value))
{
result = default(GeographicLocation);
return false;
}
if (culture == null) culture = CultureInfo.CurrentCulture;
var parts = value.Split(new[] { culture.TextInfo.ListSeparator }, StringSplitOptions.RemoveEmptyEntries).Take(3).ToArray();
if (parts.Length != 2)
{
result = default(GeographicLocation);
return false;
}
double? latitude = null;
double? longitude = null;
foreach (var part in parts.Select(p => p.Trim()))
{
if (part.EndsWith("N", StringComparison.OrdinalIgnoreCase))
{
if (latitude != null)
{
result = default(GeographicLocation);
return false;
}
latitude = ParseCoordinate(part.Substring(0, part.Length - 1), culture);
if (latitude == null)
{
result = default(GeographicLocation);
return false;
}
}
else if (part.EndsWith("S", StringComparison.OrdinalIgnoreCase))
{
if (latitude != null)
{
result = default(GeographicLocation);
return false;
}
latitude = -ParseCoordinate(part.Substring(0, part.Length - 1), culture);
if (latitude == null)
{
result = default(GeographicLocation);
return false;
}
}
else if (part.EndsWith("W", StringComparison.OrdinalIgnoreCase))
{
if (longitude != null)
{
result = default(GeographicLocation);
return false;
}
longitude = -ParseCoordinate(part.Substring(0, part.Length - 1), culture);
if (longitude == null)
{
result = default(GeographicLocation);
return false;
}
}
else if (part.EndsWith("E", StringComparison.OrdinalIgnoreCase))
{
if (longitude != null)
{
result = default(GeographicLocation);
return false;
}
longitude = ParseCoordinate(part.Substring(0, part.Length - 1), culture);
if (longitude == null)
{
result = default(GeographicLocation);
return false;
}
}
else
{
if (latitude == null)
{
latitude = ParseCoordinate(part, culture);
if (latitude == null)
{
result = default(GeographicLocation);
return false;
}
}
else if (longitude == null)
{
longitude = ParseCoordinate(part, culture);
if (longitude == null)
{
result = default(GeographicLocation);
return false;
}
}
else
{
result = default(GeographicLocation);
return false;
}
}
}
if (latitude == null || longitude == null)
{
result = default(GeographicLocation);
return false;
}
result = new GeographicLocation(latitude.Value, longitude.Value);
return true;
}
/// <summary>
/// Attempts to parse the string as a geographic location.
/// </summary>
/// <param name="value">
/// The string to parse.
/// </param>
/// <param name="result">
/// When this method returns, contains the parsed <see cref="GeographicLocation"/>, if available.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the value was parsed;
/// otherwise, <see langword="false"/>.
/// </returns>
public static bool TryParse(string value, out GeographicLocation result)
{
return TryParse(value, CultureInfo.CurrentCulture, out result);
}
/// <summary>
/// Parses the string as a geographic location.
/// </summary>
/// <param name="value">
/// The string to parse.
/// </param>
/// <param name="culture">
/// A <see cref="CultureInfo"/> which determines how values are parsed.
/// </param>
/// <returns>
/// The <see cref="GeographicLocation"/> parsed from the string.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="value"/> is <see langword="null"/> or <see cref="string.Empty"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="value"/> consists entirely of white-space.
/// </exception>
/// <exception cref="FormatException">
/// <paramref name="value"/> is not a valid geographic location.
/// </exception>
public static GeographicLocation Parse(string value, CultureInfo culture)
{
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException("value");
GeographicLocation result;
if (TryParse(value, culture, out result)) return result;
throw new FormatException();
}
/// <summary>
/// Parses the string as a geographic location.
/// </summary>
/// <param name="value">
/// The string to parse.
/// </param>
/// <returns>
/// The <see cref="GeographicLocation"/> parsed from the string.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="value"/> is <see langword="null"/> or <see cref="string.Empty"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="value"/> consists entirely of white-space.
/// </exception>
/// <exception cref="FormatException">
/// <paramref name="value"/> is not a valid geographic location.
/// </exception>
public static GeographicLocation Parse(string value)
{
return Parse(value, CultureInfo.CurrentCulture);
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>
/// A 32-bit signed integer that is the hash code for this instance.
/// </returns>
public override int GetHashCode()
{
unchecked
{
return (_latitude.GetHashCode() * 397) ^ _longitude.GetHashCode();
}
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">
/// Another object to compare to.
/// </param>
/// <returns>
/// <see langword="true"/> if <paramref name="obj"/> and this instance are the same type and represent the same value;
/// otherwise, <see langword="false"/>.
/// </returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (!(obj is GeographicLocation)) return false;
return Equals((GeographicLocation)obj);
}
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
/// <param name="other">
/// An object to compare with this object.
/// </param>
/// <returns>
/// <see langword="true"/> if the current object is equal to the <paramref name="other"/> parameter;
/// otherwise, <see langword="false"/>.
/// </returns>
public bool Equals(GeographicLocation other)
{
return Math.Abs(other._latitude - this._latitude) < Epsilon
&& Math.Abs(other._longitude - this._longitude) < Epsilon;
}
/// <summary>
/// Implements the equality operator.
/// </summary>
/// <param name="left">
/// The left operand.
/// </param>
/// <param name="right">
/// The right operand.
/// </param>
/// <returns>
/// <see langword="true"/> if <paramref name="left"/> is equal to <paramref name="right"/>;
/// otherwise, <see langword="false"/>.
/// </returns>
public static bool operator ==(GeographicLocation left, GeographicLocation right)
{
return left.Equals(right);
}
/// <summary>
/// Implements the inequality operator.
/// </summary>
/// <param name="left">
/// The left operand.
/// </param>
/// <param name="right">
/// The right operand.
/// </param>
/// <returns>
/// <see langword="true"/> if <paramref name="left"/> is not equal to <paramref name="right"/>;
/// otherwise, <see langword="false"/>.
/// </returns>
public static bool operator !=(GeographicLocation left, GeographicLocation right)
{
return !left.Equals(right);
}
}
}
@KeesHiemstra
Copy link

There is a small error in GeographicLocation.cs.
Line 384 should be: if (!double.TryParse(minutes, NumberStyles.Number | NumberStyles.AllowExponent, provider, out m))
Line 394 should be: if (!double.TryParse(seconds, NumberStyles.Number | NumberStyles.AllowExponent, provider, out s))

@RichardD2
Copy link
Author

@KeesHiemstra: Well spotted. It's fixed now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment