Конвертировать DateTime в юлианскую дату в C# (ToOADate Safe?)

Мне нужно преобразовать стандартную григорианскую дату в число юлианских дней.

Я не видел ничего задокументированного в C#, чтобы сделать это напрямую, но я нашел много постов (в то время как поиск в Google), предлагающих использовать ToOADate.

Документация по ToOADate не предлагает это в качестве допустимого метода преобразования для юлианских дат.

Может кто-нибудь уточнить, будет ли эта функция выполнять преобразование точно, или, возможно, более подходящий метод для преобразования DateTime в строку в формате Юлиана.


Это дает мне ожидаемое число при проверке на странице Юлианского дня Википедии

public static long ConvertToJulian(DateTime Date)
{
    int Month = Date.Month;
    int Day = Date.Day;
    int Year = Date.Year;

    if (Month < 3)
    {
        Month = Month + 12;
        Year = Year - 1;
    }
    long JulianDay = Day + (153 * Month - 457) / 5 + 365 * Year + (Year / 4) - (Year / 100) + (Year / 400) + 1721119;
    return JulianDay;
}

Тем не менее, это без понимания используемых магических чисел.

Спасибо


Рекомендации:

12 ответов

Решение

OADate похож на юлианские даты, но использует другую отправную точку (30 декабря 1899 г. по сравнению с 1 января 4713 г. до н.э.) и другую точку "нового дня". Джулиан Дэйтс считает, что полдень является началом нового дня, а OADates используют современное определение - полночь.

Юлианская дата полуночи 30 декабря 1899 года -2415018,5. Этот метод должен дать вам правильные значения:

public static double ToJulianDate(this DateTime date)
{
    return date.ToOADate() + 2415018.5;
}

Что касается алгоритма:

  • if (Month < 3) ...: Чтобы магические числа работали правильно, они ставят февраль в "конец" года.
  • (153 * Month - 457) / 5Ого, это серьезные магические числа.
    • Обычно количество дней в каждом месяце составляет 31 28 31 30 31 30 31 31 30 31 30 31, но после этой корректировки в операторе if оно становится 31 30 31 30 31 31 30 31 30 31 31 28. Или вычесть 30, и в итоге вы получите 1 0 1 0 1 1 0 1 0 1 1 -2. Они создают этот шаблон из 1 и 0, делая это деление в целочисленном пространстве.
    • Переписан с плавающей точкой, было бы (int)(30.6 * Month - 91.4), 30,6 - это среднее количество дней в месяце, исключая февраль (точнее, 30,63 повторения). 91,4 - это почти число дней в трех средних не февральских месяцах. (30,6 * 3 - 91,8).
    • Итак, давайте уберем 30 и сосредоточимся на этих 0,6 днях. Если мы умножим его на количество месяцев, а затем урежем до целого числа, мы получим комбинацию из 0 и 1.
      • 0,6 * 0 = 0,0 -> 0
      • 0,6 * 1 = 0,6 -> 0 (разница 0)
      • 0,6 * 2 = 1,2 -> 1 (разница 1)
      • 0,6 * 3 = 1,8 -> 1 (разница 0)
      • 0,6 * 4 = 2,4 -> 2 (разница 1)
      • 0,6 * 5 = 3,0 -> 3 (разница 1)
      • 0,6 * 6 = 3,6 -> 3 (разница 0)
      • 0,6 * 7 = 4,2 -> 4 (разница 1)
      • 0,6 * 8 = 4,8 -> 4 (разница 0)
    • Видите эту картину различий в праве? Это та же самая схема в списке выше, количество дней в каждом месяце минус 30. Вычитание 91,8 компенсирует количество дней в первых трех месяцах, которые были перенесены на "конец" года, и корректировку он на 0,4 сдвигает последовательные разности 1 (0,6 * 4 и 0,6 * 5 в приведенной выше таблице) для выравнивания с соседними месяцами, которые составляют 31 день.
    • Так как февраль сейчас в "конце" года, нам не нужно иметь дело с его продолжительностью. Это может длиться 45 дней (46 в високосный год), и единственное, что должно измениться, - это константа для количества дней в году, 365.
    • Обратите внимание, что это зависит от модели дней 30 и 31 месяц. Если бы у нас было два месяца подряд, по 30 дней, это было бы невозможно.
  • 365 * Year: Дней в году
  • (Year / 4) - (Year / 100) + (Year / 400): Плюс один високосный день каждые 4 года, минус один каждые 100, плюс один каждые 400 лет.
  • + 1721119Это юлианская дата 2 марта 1 г. до н.э. Поскольку мы перенесли "начало" календаря с января на март, мы используем это в качестве нашего смещения, а не 1 января. Поскольку нулевого года нет, 1 до н.э. получает целочисленное значение 0. Что касается того, что 2 марта вместо 1 марта, я предполагаю, что это потому, что вычисление целого месяца в конце все еще было немного неубедительным. Если автор использовал - 462 вместо - 457 (- 92.4 вместо - 91.4 в математике с плавающей точкой), то смещение было бы до 1 марта.

Пока метод

public static double ToJulianDate(this DateTime date) { return date.ToOADate() + 2415018.5; }

Работает на современные даты, имеет существенные недостатки.

Юлианская дата определена для отрицательных дат - то есть, дат до нашей эры (до общей эры) и является обычной в астрономических расчетах. Вы не можете создать объект DateTime с годом меньше 0, и поэтому юлианская дата не может быть вычислена для дат BCE с использованием вышеуказанного метода.

Реформа григорианского календаря 1582 года создала 11-дневную дыру в календаре между 4 и 15 октября. Эти даты не определены ни в юлианском календаре, ни в григорианском календаре, но DateTime принимает их в качестве аргументов. Кроме того, использование вышеуказанного метода не возвращает правильное значение для любой юлианской даты. Эксперименты с использованием System.Globalization.JulianCalendar.ToDateTime() или передачей эры JulianCalendar в конструктор DateTime по-прежнему дают неверные результаты для всех дат до 5 октября 1582 года.

Следующие процедуры, адаптированные из "Астрономических алгоритмов" Жана Миуса, возвращают правильные результаты для всех дат, начиная с полудня 1 января, -4712, нулевого времени в юлианском календаре. Они также генерируют ArgumentOutOfRangeException, если передана недопустимая дата.

 public class JulianDate
{
    public static bool isJulianDate(int year, int month, int day)
    {
        // All dates prior to 1582 are in the Julian calendar
        if (year < 1582)
            return true;
        // All dates after 1582 are in the Gregorian calendar
        else if (year > 1582)
            return false;
        else
        {
            // If 1582, check before October 4 (Julian) or after October 15 (Gregorian)
            if (month < 10)
                return true;
            else if (month > 10)
                return false;
            else
            {
                if (day < 5)
                    return true;
                else if (day > 14)
                    return false;
                else
                    // Any date in the range 10/5/1582 to 10/14/1582 is invalid 
                    throw new ArgumentOutOfRangeException(
                        "This date is not valid as it does not exist in either the Julian or the Gregorian calendars.");
            }
        }
    }

    static private double DateToJD(int year, int month, int day, int hour, int minute, int second, int millisecond)
    {
        // Determine correct calendar based on date
        bool JulianCalendar = isJulianDate(year, month, day);

        int M = month > 2 ? month : month + 12;
        int Y = month > 2 ? year : year - 1;
        double D = day + hour/24.0 + minute/1440.0 + (second + millisecond / 1000.0)/86400.0;
        int B = JulianCalendar ? 0 : 2 - Y/100 + Y/100/4;

        return (int) (365.25*(Y + 4716)) + (int) (30.6001*(M + 1)) + D + B - 1524.5;
    }

    static public double JD(int year, int month, int day, int hour, int minute, int second, int millisecond)
    {
        return DateToJD(year, month, day, hour, minute, second, millisecond);
    }


    static public double JD(DateTime date) 
    {
        return DateToJD(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Millisecond);
    }
}

Если кому-то нужно преобразовать юлианскую дату в DateTime, см. Ниже:

public static DateTime FromJulianDate(double julianDate)
{
    return DateTime.FromOADate(julianDate - 2415018.5);
}

Объяснения Дэвида Йоу точны, но расчет кумулятивного числа дней в году за месяцы, предшествующие данному месяцу, не интуитивен. Если вы предпочитаете массив целых чисел, чтобы сделать алгоритм более понятным, то это будет делать:

    /*
     * convert magic numbers created by:
     *    (153*month - 457)/5) 
     * into an explicit array of integers
     */
    int[] CumulativeDays = new int[]
    {
        -92   // Month = 0  (Should not be accessed by algorithm)
      , -61   // Month = 1  (Should not be accessed by algorithm)
      , -31   // Month = 2  (Should not be accessed by algorithm)
      ,   0   // Month = 3  (March)
      ,  31   // Month = 4  (April)
      ,  61   // Month = 5  (May)
      ,  92   // Month = 6  (June)
      , 122   // Month = 7  (July)
      , 153   // Month = 8  (August)
      , 184   // Month = 9  (September)
      , 214   // Month = 10 (October)
      , 245   // Month = 11 (November)
      , 275   // Month = 12 (December)
      , 306   // Month = 13 (January, next year)
      , 337   // Month = 14 (February, next year)
    };

и первые три строки расчета становятся:

  int julianDay = day
                  + CumulativeDays[month]
                  + 365*year
                  + (year/4)

Выражение

(153*month - 457)/5)

хотя производит ту же самую последовательность тех же целых чисел, что и массив выше для значений в диапазоне: от 3 до 14; включительно и делает это без требований к хранению. Отсутствие требований к хранению является лишь достоинством при расчете совокупного количества дней таким и запутанным способом.

Приведенный ниже метод дает вам юлианские дни, начиная с 1 января 1995 г., 00:00:00.

          /// <summary>
    /// "GetJulianDays" will return a Julian Days starting from date 1 Jan 1995
    /// </summary>
    /// <param name="YYYYMMddHHmmss"></param>
    /// <returns>Julian Day for given date</returns>
    public string GetJulianDays(DateTime YYYYMMddHHmmss)
    {
        string DateTimeInJulianFormat = string.Empty;
        DateTime julianStartDate = new DateTime(1995, 1, 1, 00, 00, 00); //YYYY,MM,dd,HH,mm,ss

        DateTime DateTimeNow = YYYYMMddHHmmss;

        double difference = (DateTimeNow - julianStartDate).TotalDays;

        int totalDays = int.Parse(difference.ToString());

        DateTimeInJulianFormat = string.Format("{0:X}", totalDays);

        return DateTimeInJulianFormat;
    }

Мой код для модифицированной юлианской даты использует тот же алгоритм, но с другим магическим числом на конце, так что полученное значение соответствует модифицированной юлианской дате, показанной в Википедии. Я использую этот же алгоритм в течение как минимум 10 лет в качестве ключа для ежедневных временных рядов (изначально на Java).

public static int IntegerDate(DateTime date)
    {
        int Month = date.Month;
        int Day = date.Day;
        int Year = date.Year;

        if (Month < 3)
        {
            Month = Month + 12;
            Year = Year - 1;
        }
        //modified Julian Date
        return Day + (153 * Month - 457) / 5 + 365 * Year + (Year / 4) - (Year / 100) + (Year / 400) - 678882;
    }

Обратный расчет имеет больше магических чисел для вашего развлечения:

public static DateTime FromDateInteger(int mjd)
    {
        long a = mjd + 2468570;
        long b = (long)((4 * a) / 146097);
        a = a - ((long)((146097 * b + 3) / 4));
        long c = (long)((4000 * (a + 1) / 1461001));
        a = a - (long)((1461 * c) / 4) + 31;
        long d = (long)((80 * a) / 2447);
        int Day = (int)(a - (long)((2447 * d) / 80));
        a = (long)(d / 11);
        int Month = (int)(d + 2 - 12 * a);
        int Year = (int)(100 * (b - 49) + c + a);
        return new DateTime(Year, Month, Day);
    }

на разных страницах:

код:

      ViewData["jul"] = DateTime.Now.ToOADate() + 2415018.5;

вид:

      @ViewData["jul"]

только на виду:

      @{Double jday= DateTime.Now.ToOADate() + 2415018.5;}
@jday

Я использую некоторые вычисления в микроконтроллерах, но мне нужны годы между 2000 и 2255 годами. Вот мой код:

typedef struct {
    unsigned int8   seconds;    // 0 to 59
    unsigned int8   minutes;    // 0 to 59
    unsigned int8   hours;      // 0 to 23  (24-hour time)
    unsigned int8   day;        // 1 to 31
    unsigned int8   weekday;    // 0 = Sunday, 1 = Monday, etc.
    unsigned int8   month;      // 1 to 12
    unsigned int8   year;       // (2)000 to (2)255
    unsigned int32  julian;     // Julian date
} date_time_t;

    

// Преобразование из DD-MM-YY HH:MM:SS в JulianTime

void JulianTime(date_time_t * dt)
{
    unsigned int8   m, y;

    y = dt->year;
    m = dt->month;
    if (m > 2) m -= 3;
    else {
        m +=  9;
        y --;
    }
    dt->julian  = ((1461 * y) / 4) + ((153 * m + 2) / 5) + dt->day;
    dt->weekday = ( dt->julian + 2 ) % 7;
    dt->julian  = (dt->julian * 24) + (dt->hours   ); 
    dt->julian  = (dt->julian * 60) + (dt->minutes );     
    dt->julian  = (dt->julian * 60) + (dt->seconds );     
}

// Обратный переход от JulianTime к DD-MM-YY HH:MM:SS

void GregorianTime(date_time_t *dt) 
{
    unsigned int32  j = dt->julian;

    dt->seconds = j % 60;
    j /= 60;
    dt->minutes = j % 60;
    j /= 60;
    dt->hours   = j % 24;
    j /= 24;
    dt->weekday = ( j + 2 ) % 7; // Get day of week
    dt->year = (4 * j) / 1461;
    j = j - ((1461 * dt->year) / 4);
    dt->month = (5 * j - 3) / 153;
    dt->day  = j - (((dt->month * 153) + 3) / 5);
    if ( dt->month < 10 )
    {
        dt->month += 3;
    }
    else
    {
        dt->month -= 9;
        dt->year ++;
    }
}

Надеюсь, это поможет:D

По определению от 1 января 2000 г. в 11:58:55800 UTC (J2000.0)

ровно 2451545 JD (юлианских дней) прошло с самого первого дня.

      const long J2000UtcTicks = 630823247358000000L; // (new DateTime(2000,1,1,11,58,55,800)).Ticks
const double TicksPerDay = 24 * 60 * 60 * 1E7; // 100ns is equal to 1 tick

// to convert any
DateTime dt;

// you need to convert to timezone GMT and calc the ticks ...
double ticks = dt.ToUniversalTime().Ticks - J2000UtcTicks;
return 2451545d + ticks / TicksPerDay;

Страница Википедии, на которую вы ссылаетесь, содержит код для преобразования из юлианского или григорианского календарей. Например, вы можете преобразовать дату, предшествующую эпохе григорианского календаря, которая называется «пролептическим григорианским календарем».

В зависимости от выбранного календаря «конверсии» результат будет отличаться. Это связано с тем, что сами календари представляют собой разные конструкции и по-разному относятся к выравниванию / исправлению различного рода.

      public enum ConversionCalendar
{
  GregorianCalendar,
  JulianCalendar,
}

public static int ConvertDatePartsToJdn(int year, int month, int day, ConversionCalendar conversionCalendar)
{
  switch (conversionCalendar)
  {
    case ConversionCalendar.GregorianCalendar:
      return ((1461 * (year + 4800 + (month - 14) / 12)) / 4 + (367 * (month - 2 - 12 * ((month - 14) / 12))) / 12 - (3 * ((year + 4900 + (month - 14) / 12) / 100)) / 4 + day - 32075);
    case ConversionCalendar.JulianCalendar:
      return (367 * year - (7 * (year + 5001 + (month - 9) / 7)) / 4 + (275 * month) / 9 + day + 1729777);
    default:
      throw new System.ArgumentOutOfRangeException(nameof(calendar));
  }
}

Можно также преобразовать компоненты JDN на сегодняшний день:

      public static void ConvertJdnToDateParts(int julianDayNumber, ConversionCalendar conversionCalendar, out int year, out int month, out int day)
{
  var f = julianDayNumber + 1401;

  if (conversionCalendar == ConversionCalendar.GregorianCalendar)
    f += (4 * julianDayNumber + 274277) / 146097 * 3 / 4 + -38;

  var eq = System.Math.DivRem(4 * f + 3, 1461, out var er);
  var hq = System.Math.DivRem(5 * (er / 4) + 2, 153, out var hr);

  day = hr / 5 + 1;
  month = ((hq + 2) % 12) + 1;
  year = eq - 4716 + (14 - month) / 12;
}

Эти методы были созданы из кода в Википедии, поэтому они должны работать, если я что-то не наткнулся.

Хорошо, поэтому я использовал юлианские даты для хранения их в базах данных SQLite. Хорошей новостью является то, что в эту библиотеку встроена поддержка таких дат. Вы можете использовать их без написания дополнительного кода. Итак, вот чтобы это запустить:

Шаг 1. Загрузите нужную DLL: System.Data.Sqlite.DLL . Вы можете найти это в Интернете по адресу www.dll-files.com . Я взял 64-битную версию.

Шаг 2: Сделайте ссылку на эту DLL в своем проекте.

Шаг 3. При запуске вы можете получить раздражающее сообщение о том, что библиотека работает в смешанном режиме и построена на основе более ранней платформы. Чтобы решить эту проблему, откройте app.config и измените эту строку

        <startup>

в

        <startup useLegacyV2RuntimeActivationPolicy="true"> 

Теперь вам пора идти. Пример кода (конечно, его можно сократить с помощью директивы using):

        var d = new DateTime(2023, 5, 25, 16, 11, 30);
  Debug.Print(d.ToString());
  var j = System.Data.SQLite.SQLiteConvert.ToJulianDay(d);
  Debug.Print(j.ToString());
  var d2 = System.Data.SQLite.SQLiteConvert.ToDateTime(j, DateTimeKind.Local);
  Debug.Print(d2.ToString());

И вывод из этого:

       2023-05-25 16:11:30
 2460090.17465278
 2023-05-25 16:11:30

Следующая функция преобразует дату в юлианскую дату, соответствующую дате Tradestation Easylanguage: public double ToJulianDate(DateTime date) { return date.ToOADate(); }

Другие вопросы по тегам