Last active
January 7, 2020 09:49
-
-
Save RichardD2/f6b08a5791b21ac77ce7 to your computer and use it in GitHub Desktop.
Sunrise / Sunset calculation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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))