More Corelib cleanup (dotnet/coreclr#26872)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Globalization / CalendricalCalculationsHelper.cs
blob99caf6ba1d1baad6780b3e199862117ad3a0fd66
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Diagnostics;
7 namespace System.Globalization
9 internal static class CalendricalCalculationsHelper
11 private const double FullCircleOfArc = 360.0; // 360.0;
12 private const int HalfCircleOfArc = 180;
13 private const double TwelveHours = 0.5; // half a day
14 private const double Noon2000Jan01 = 730120.5;
15 internal const double MeanTropicalYearInDays = 365.242189;
16 private const double MeanSpeedOfSun = MeanTropicalYearInDays / FullCircleOfArc;
17 private const double LongitudeSpring = 0.0;
18 private const double TwoDegreesAfterSpring = 2.0;
19 private const int SecondsPerDay = 24 * 60 * 60; // 24 hours * 60 minutes * 60 seconds
21 private const int DaysInUniformLengthCentury = 36525;
22 private const int SecondsPerMinute = 60;
23 private const int MinutesPerDegree = 60;
25 private static readonly long s_startOf1810 = GetNumberOfDays(new DateTime(1810, 1, 1));
26 private static readonly long s_startOf1900Century = GetNumberOfDays(new DateTime(1900, 1, 1));
28 private static readonly double[] s_coefficients1900to1987 = new double[] { -0.00002, 0.000297, 0.025184, -0.181133, 0.553040, -0.861938, 0.677066, -0.212591 };
29 private static readonly double[] s_coefficients1800to1899 = new double[] { -0.000009, 0.003844, 0.083563, 0.865736, 4.867575, 15.845535, 31.332267, 38.291999, 28.316289, 11.636204, 2.043794 };
30 private static readonly double[] s_coefficients1700to1799 = new double[] { 8.118780842, -0.005092142, 0.003336121, -0.0000266484 };
31 private static readonly double[] s_coefficients1620to1699 = new double[] { 196.58333, -4.0675, 0.0219167 };
32 private static readonly double[] s_lambdaCoefficients = new double[] { 280.46645, 36000.76983, 0.0003032 };
33 private static readonly double[] s_anomalyCoefficients = new double[] { 357.52910, 35999.05030, -0.0001559, -0.00000048 };
34 private static readonly double[] s_eccentricityCoefficients = new double[] { 0.016708617, -0.000042037, -0.0000001236 };
35 private static readonly double[] s_coefficients = new double[] { Angle(23, 26, 21.448), Angle(0, 0, -46.8150), Angle(0, 0, -0.00059), Angle(0, 0, 0.001813) };
36 private static readonly double[] s_coefficientsA = new double[] { 124.90, -1934.134, 0.002063 };
37 private static readonly double[] s_coefficientsB = new double[] { 201.11, 72001.5377, 0.00057 };
39 private static double RadiansFromDegrees(double degree)
41 return degree * Math.PI / 180;
44 private static double SinOfDegree(double degree)
46 return Math.Sin(RadiansFromDegrees(degree));
49 private static double CosOfDegree(double degree)
51 return Math.Cos(RadiansFromDegrees(degree));
53 private static double TanOfDegree(double degree)
55 return Math.Tan(RadiansFromDegrees(degree));
58 public static double Angle(int degrees, int minutes, double seconds)
60 return ((seconds / SecondsPerMinute + minutes) / MinutesPerDegree) + degrees;
63 private static double Obliquity(double julianCenturies)
65 return PolynomialSum(s_coefficients, julianCenturies);
68 internal static long GetNumberOfDays(DateTime date)
70 return date.Ticks / GregorianCalendar.TicksPerDay;
73 private static int GetGregorianYear(double numberOfDays)
75 return new DateTime(Math.Min((long)(Math.Floor(numberOfDays) * GregorianCalendar.TicksPerDay), DateTime.MaxValue.Ticks)).Year;
78 private enum CorrectionAlgorithm
80 Default,
81 Year1988to2019,
82 Year1900to1987,
83 Year1800to1899,
84 Year1700to1799,
85 Year1620to1699
88 private struct EphemerisCorrectionAlgorithmMap
90 public EphemerisCorrectionAlgorithmMap(int year, CorrectionAlgorithm algorithm)
92 _lowestYear = year;
93 _algorithm = algorithm;
96 internal int _lowestYear;
97 internal CorrectionAlgorithm _algorithm;
100 private static readonly EphemerisCorrectionAlgorithmMap[] s_ephemerisCorrectionTable = new EphemerisCorrectionAlgorithmMap[]
102 // lowest year that starts algorithm, algorithm to use
103 new EphemerisCorrectionAlgorithmMap(2020, CorrectionAlgorithm.Default),
104 new EphemerisCorrectionAlgorithmMap(1988, CorrectionAlgorithm.Year1988to2019),
105 new EphemerisCorrectionAlgorithmMap(1900, CorrectionAlgorithm.Year1900to1987),
106 new EphemerisCorrectionAlgorithmMap(1800, CorrectionAlgorithm.Year1800to1899),
107 new EphemerisCorrectionAlgorithmMap(1700, CorrectionAlgorithm.Year1700to1799),
108 new EphemerisCorrectionAlgorithmMap(1620, CorrectionAlgorithm.Year1620to1699),
109 new EphemerisCorrectionAlgorithmMap(int.MinValue, CorrectionAlgorithm.Default) // default must be last
112 private static double Reminder(double divisor, double dividend)
114 double whole = Math.Floor(divisor / dividend);
115 return divisor - (dividend * whole);
118 private static double NormalizeLongitude(double longitude)
120 longitude = Reminder(longitude, FullCircleOfArc);
121 if (longitude < 0)
123 longitude += FullCircleOfArc;
125 return longitude;
128 public static double AsDayFraction(double longitude)
130 return longitude / FullCircleOfArc;
133 private static double PolynomialSum(double[] coefficients, double indeterminate)
135 double sum = coefficients[0];
136 double indeterminateRaised = 1;
137 for (int i = 1; i < coefficients.Length; i++)
139 indeterminateRaised *= indeterminate;
140 sum += (coefficients[i] * indeterminateRaised);
143 return sum;
146 private static double CenturiesFrom1900(int gregorianYear)
148 long july1stOfYear = GetNumberOfDays(new DateTime(gregorianYear, 7, 1));
149 return (double)(july1stOfYear - s_startOf1900Century) / DaysInUniformLengthCentury;
152 // the following formulas defines a polynomial function which gives us the amount that the earth is slowing down for specific year ranges
153 private static double DefaultEphemerisCorrection(int gregorianYear)
155 Debug.Assert(gregorianYear < 1620 || 2020 <= gregorianYear);
156 long january1stOfYear = GetNumberOfDays(new DateTime(gregorianYear, 1, 1));
157 double daysSinceStartOf1810 = january1stOfYear - s_startOf1810;
158 double x = TwelveHours + daysSinceStartOf1810;
159 return ((Math.Pow(x, 2) / 41048480) - 15) / SecondsPerDay;
162 private static double EphemerisCorrection1988to2019(int gregorianYear)
164 Debug.Assert(1988 <= gregorianYear && gregorianYear <= 2019);
165 return (double)(gregorianYear - 1933) / SecondsPerDay;
168 private static double EphemerisCorrection1900to1987(int gregorianYear)
170 Debug.Assert(1900 <= gregorianYear && gregorianYear <= 1987);
171 double centuriesFrom1900 = CenturiesFrom1900(gregorianYear);
172 return PolynomialSum(s_coefficients1900to1987, centuriesFrom1900);
175 private static double EphemerisCorrection1800to1899(int gregorianYear)
177 Debug.Assert(1800 <= gregorianYear && gregorianYear <= 1899);
178 double centuriesFrom1900 = CenturiesFrom1900(gregorianYear);
179 return PolynomialSum(s_coefficients1800to1899, centuriesFrom1900);
182 private static double EphemerisCorrection1700to1799(int gregorianYear)
184 Debug.Assert(1700 <= gregorianYear && gregorianYear <= 1799);
185 double yearsSince1700 = gregorianYear - 1700;
186 return PolynomialSum(s_coefficients1700to1799, yearsSince1700) / SecondsPerDay;
189 private static double EphemerisCorrection1620to1699(int gregorianYear)
191 Debug.Assert(1620 <= gregorianYear && gregorianYear <= 1699);
192 double yearsSince1600 = gregorianYear - 1600;
193 return PolynomialSum(s_coefficients1620to1699, yearsSince1600) / SecondsPerDay;
196 // ephemeris-correction: correction to account for the slowing down of the rotation of the earth
197 private static double EphemerisCorrection(double time)
199 int year = GetGregorianYear(time);
200 foreach (EphemerisCorrectionAlgorithmMap map in s_ephemerisCorrectionTable)
202 if (map._lowestYear <= year)
204 switch (map._algorithm)
206 case CorrectionAlgorithm.Default: return DefaultEphemerisCorrection(year);
207 case CorrectionAlgorithm.Year1988to2019: return EphemerisCorrection1988to2019(year);
208 case CorrectionAlgorithm.Year1900to1987: return EphemerisCorrection1900to1987(year);
209 case CorrectionAlgorithm.Year1800to1899: return EphemerisCorrection1800to1899(year);
210 case CorrectionAlgorithm.Year1700to1799: return EphemerisCorrection1700to1799(year);
211 case CorrectionAlgorithm.Year1620to1699: return EphemerisCorrection1620to1699(year);
214 break; // break the loop and assert eventually
218 Debug.Fail("Not expected to come here");
219 return DefaultEphemerisCorrection(year);
222 public static double JulianCenturies(double moment)
224 double dynamicalMoment = moment + EphemerisCorrection(moment);
225 return (dynamicalMoment - Noon2000Jan01) / DaysInUniformLengthCentury;
228 private static bool IsNegative(double value)
230 return Math.Sign(value) == -1;
233 private static double CopySign(double value, double sign)
235 return (IsNegative(value) == IsNegative(sign)) ? value : -value;
238 // equation-of-time; approximate the difference between apparent solar time and mean solar time
239 // formal definition is EOT = GHA - GMHA
240 // GHA is the Greenwich Hour Angle of the apparent (actual) Sun
241 // GMHA is the Greenwich Mean Hour Angle of the mean (fictitious) Sun
242 // http://www.esrl.noaa.gov/gmd/grad/solcalc/
243 // http://en.wikipedia.org/wiki/Equation_of_time
244 private static double EquationOfTime(double time)
246 double julianCenturies = JulianCenturies(time);
247 double lambda = PolynomialSum(s_lambdaCoefficients, julianCenturies);
248 double anomaly = PolynomialSum(s_anomalyCoefficients, julianCenturies);
249 double eccentricity = PolynomialSum(s_eccentricityCoefficients, julianCenturies);
251 double epsilon = Obliquity(julianCenturies);
252 double tanHalfEpsilon = TanOfDegree(epsilon / 2);
253 double y = tanHalfEpsilon * tanHalfEpsilon;
255 double dividend = ((y * SinOfDegree(2 * lambda))
256 - (2 * eccentricity * SinOfDegree(anomaly))
257 + (4 * eccentricity * y * SinOfDegree(anomaly) * CosOfDegree(2 * lambda))
258 - (0.5 * Math.Pow(y, 2) * SinOfDegree(4 * lambda))
259 - (1.25 * Math.Pow(eccentricity, 2) * SinOfDegree(2 * anomaly)));
260 const double Divisor = 2 * Math.PI;
261 double equation = dividend / Divisor;
263 // approximation of equation of time is not valid for dates that are many millennia in the past or future
264 // thus limited to a half day
265 return CopySign(Math.Min(Math.Abs(equation), TwelveHours), equation);
268 private static double AsLocalTime(double apparentMidday, double longitude)
270 // slightly inaccurate since equation of time takes mean time not apparent time as its argument, but the difference is negligible
271 double universalTime = apparentMidday - AsDayFraction(longitude);
272 return apparentMidday - EquationOfTime(universalTime);
275 // midday
276 public static double Midday(double date, double longitude)
278 return AsLocalTime(date + TwelveHours, longitude) - AsDayFraction(longitude);
281 private static double InitLongitude(double longitude)
283 return NormalizeLongitude(longitude + HalfCircleOfArc) - HalfCircleOfArc;
286 // midday-in-tehran
287 public static double MiddayAtPersianObservationSite(double date)
289 return Midday(date, InitLongitude(52.5)); // 52.5 degrees east - longitude of UTC+3:30 which defines Iranian Standard Time
292 private static double PeriodicTerm(double julianCenturies, int x, double y, double z)
294 return x * SinOfDegree(y + z * julianCenturies);
297 private static double SumLongSequenceOfPeriodicTerms(double julianCenturies)
299 double sum = 0.0;
300 sum += PeriodicTerm(julianCenturies, 403406, 270.54861, 0.9287892);
301 sum += PeriodicTerm(julianCenturies, 195207, 340.19128, 35999.1376958);
302 sum += PeriodicTerm(julianCenturies, 119433, 63.91854, 35999.4089666);
303 sum += PeriodicTerm(julianCenturies, 112392, 331.2622, 35998.7287385);
304 sum += PeriodicTerm(julianCenturies, 3891, 317.843, 71998.20261);
305 sum += PeriodicTerm(julianCenturies, 2819, 86.631, 71998.4403);
306 sum += PeriodicTerm(julianCenturies, 1721, 240.052, 36000.35726);
307 sum += PeriodicTerm(julianCenturies, 660, 310.26, 71997.4812);
308 sum += PeriodicTerm(julianCenturies, 350, 247.23, 32964.4678);
309 sum += PeriodicTerm(julianCenturies, 334, 260.87, -19.441);
310 sum += PeriodicTerm(julianCenturies, 314, 297.82, 445267.1117);
311 sum += PeriodicTerm(julianCenturies, 268, 343.14, 45036.884);
312 sum += PeriodicTerm(julianCenturies, 242, 166.79, 3.1008);
313 sum += PeriodicTerm(julianCenturies, 234, 81.53, 22518.4434);
314 sum += PeriodicTerm(julianCenturies, 158, 3.5, -19.9739);
315 sum += PeriodicTerm(julianCenturies, 132, 132.75, 65928.9345);
316 sum += PeriodicTerm(julianCenturies, 129, 182.95, 9038.0293);
317 sum += PeriodicTerm(julianCenturies, 114, 162.03, 3034.7684);
318 sum += PeriodicTerm(julianCenturies, 99, 29.8, 33718.148);
319 sum += PeriodicTerm(julianCenturies, 93, 266.4, 3034.448);
320 sum += PeriodicTerm(julianCenturies, 86, 249.2, -2280.773);
321 sum += PeriodicTerm(julianCenturies, 78, 157.6, 29929.992);
322 sum += PeriodicTerm(julianCenturies, 72, 257.8, 31556.493);
323 sum += PeriodicTerm(julianCenturies, 68, 185.1, 149.588);
324 sum += PeriodicTerm(julianCenturies, 64, 69.9, 9037.75);
325 sum += PeriodicTerm(julianCenturies, 46, 8.0, 107997.405);
326 sum += PeriodicTerm(julianCenturies, 38, 197.1, -4444.176);
327 sum += PeriodicTerm(julianCenturies, 37, 250.4, 151.771);
328 sum += PeriodicTerm(julianCenturies, 32, 65.3, 67555.316);
329 sum += PeriodicTerm(julianCenturies, 29, 162.7, 31556.08);
330 sum += PeriodicTerm(julianCenturies, 28, 341.5, -4561.54);
331 sum += PeriodicTerm(julianCenturies, 27, 291.6, 107996.706);
332 sum += PeriodicTerm(julianCenturies, 27, 98.5, 1221.655);
333 sum += PeriodicTerm(julianCenturies, 25, 146.7, 62894.167);
334 sum += PeriodicTerm(julianCenturies, 24, 110.0, 31437.369);
335 sum += PeriodicTerm(julianCenturies, 21, 5.2, 14578.298);
336 sum += PeriodicTerm(julianCenturies, 21, 342.6, -31931.757);
337 sum += PeriodicTerm(julianCenturies, 20, 230.9, 34777.243);
338 sum += PeriodicTerm(julianCenturies, 18, 256.1, 1221.999);
339 sum += PeriodicTerm(julianCenturies, 17, 45.3, 62894.511);
340 sum += PeriodicTerm(julianCenturies, 14, 242.9, -4442.039);
341 sum += PeriodicTerm(julianCenturies, 13, 115.2, 107997.909);
342 sum += PeriodicTerm(julianCenturies, 13, 151.8, 119.066);
343 sum += PeriodicTerm(julianCenturies, 13, 285.3, 16859.071);
344 sum += PeriodicTerm(julianCenturies, 12, 53.3, -4.578);
345 sum += PeriodicTerm(julianCenturies, 10, 126.6, 26895.292);
346 sum += PeriodicTerm(julianCenturies, 10, 205.7, -39.127);
347 sum += PeriodicTerm(julianCenturies, 10, 85.9, 12297.536);
348 sum += PeriodicTerm(julianCenturies, 10, 146.1, 90073.778);
349 return sum;
352 private static double Aberration(double julianCenturies)
354 return (0.0000974 * CosOfDegree(177.63 + (35999.01848 * julianCenturies))) - 0.005575;
357 private static double Nutation(double julianCenturies)
359 double a = PolynomialSum(s_coefficientsA, julianCenturies);
360 double b = PolynomialSum(s_coefficientsB, julianCenturies);
361 return (-0.004778 * SinOfDegree(a)) - (0.0003667 * SinOfDegree(b));
364 public static double Compute(double time)
366 double julianCenturies = JulianCenturies(time);
367 double lambda = 282.7771834
368 + (36000.76953744 * julianCenturies)
369 + (0.000005729577951308232 * SumLongSequenceOfPeriodicTerms(julianCenturies));
371 double longitude = lambda + Aberration(julianCenturies) + Nutation(julianCenturies);
372 return InitLongitude(longitude);
375 public static double AsSeason(double longitude)
377 return (longitude < 0) ? (longitude + FullCircleOfArc) : longitude;
380 private static double EstimatePrior(double longitude, double time)
382 double timeSunLastAtLongitude = time - (MeanSpeedOfSun * AsSeason(InitLongitude(Compute(time) - longitude)));
383 double longitudeErrorDelta = InitLongitude(Compute(timeSunLastAtLongitude) - longitude);
384 return Math.Min(time, timeSunLastAtLongitude - (MeanSpeedOfSun * longitudeErrorDelta));
387 // persian-new-year-on-or-before
388 // number of days is the absolute date. The absolute date is the number of days from January 1st, 1 A.D.
389 // 1/1/0001 is absolute date 1.
390 internal static long PersianNewYearOnOrBefore(long numberOfDays)
392 double date = (double)numberOfDays;
394 double approx = EstimatePrior(LongitudeSpring, MiddayAtPersianObservationSite(date));
395 long lowerBoundNewYearDay = (long)Math.Floor(approx) - 1;
396 long upperBoundNewYearDay = lowerBoundNewYearDay + 3; // estimate is generally within a day of the actual occurrence (at the limits, the error expands, since the calculations rely on the mean tropical year which changes...)
397 long day = lowerBoundNewYearDay;
398 for (; day != upperBoundNewYearDay; ++day)
400 double midday = MiddayAtPersianObservationSite((double)day);
401 double l = Compute(midday);
402 if ((LongitudeSpring <= l) && (l <= TwoDegreesAfterSpring))
404 break;
407 Debug.Assert(day != upperBoundNewYearDay);
409 return day - 1;