需要一个公式:从自公元1年1月1日午夜开始的秒数中提取年份

8

输入:从公元0001年1月1日开始的秒数

输出:这段时间内经过的完整年数

我已经开发了一个算法,但我认为它不是最优解。我认为应该有一种方法可以避免使用循环。见代码块1中的算法,它A)确定天数和B)在递增年份总数的同时迭代地减去366或365(取决于闰年)。

这不像简单地将DayCount除以365.2425并截断那么简单,因为我们在公元0002年1月1日达到失败点。(31536000秒/(365.2425 * 24 * 60 * 60)= 0.99934)。

有没有想法可以从自公元0001年1月1日12:00 AM以来的秒数中提取年份而不需要循环?

我需要找出这个答案,因为我需要将日期嵌入long(存储秒数),以便我可以精确追踪12百万秒内的年份。

代码块1 - 低效的算法从秒中获取年份(包括闰年)

        Dim Days, Years As Integer

        'get Days
        Days = Ticks * (1 / 24) * (1 / 60) * (1 / 60) 'Ticks = Seconds from Year 1, January 1

        'get years by counting up from the beginning
        Years = 0
        While True
            'if leap year
            If (Year Mod 4 = 0) AndAlso (Year Mod 100 <> 0) OrElse (Year Mod 400 = 0) Then
                If Days >= 366 Then 'if we have enough days left to increment the year
                    Years += 1
                    Days -= 366
                Else
                    Exit While
                End If
                'if not leap  year
            Else
                If Days >= 365 Then 'if we have enough days left to increment the year
                    Years += 1
                    Days -= 365
                Else
                    Exit While
                End If
            End If
        End While

        Return Years

编辑:我的解决方案是放弃使用8位内嵌日期的内存节省,而是将每个值(从秒到年)分别存储在不同的整数中。这会导致内存消耗增加,但可以立即检索。
编辑2:第一次编辑中有一个错字(8位)。

5
你是否知晓历法改革曾导致有几天被跳过? - Thilo
2
不要忘记闰秒! :D - Miles
我感到困惑,为什么你不使用时间戳? - JSchlather
@Liberalkid - 不确定你指的是什么,但我需要在8个字节内嵌入1200万年的1秒精度。 - Brian Webster
@Thilo - 这是一个很好的观点,尽管对于这个结构来说并不重要。我可以从1900年开始,并更加技术上正确。即使有闰秒也不会有任何影响。主要问题是我需要将其编码为8字节(long),解码后保持相同的值(并处理2月29日的闰年)。使用循环计算这个问题相当简单,但我真的不想通过循环迭代1到12百万次来解码一年。显然,我可能试图用过多的处理能力来换取太少的存储空间。 - Brian Webster
7个回答

25

如果你需要精确到秒,你将需要一款商业级日期时间包;使用简单算法准确处理这些复杂性太难了。例如:

  • 还有闰秒的存在。某个管理机构会任意决定每年是否需要在时钟上增加一秒(有时候是两秒)。无法提前知道何时会出现下一个闰秒,过去的闰秒也没有规律可循。
  • 由于这些以及其他的复杂性,除非您可以放宽对精度要求到最后一秒12百万年的范围内,否则最好不要自己编写代码。

    "1582年10月4日 - 圣特蕾莎·阿维拉逝世。她于次日10月15日被埋葬。"


    5

    维基百科有一篇关于儒略日的文章,其中包含一个算法,您可以根据自己的需求进行调整。


    这个可能可行,我会再仔细研究一下。如果我能够得到一个O(1)算法来将1900年以来的秒数转换为儒略日再转换为公历日期,那么我们就可以解决问题了(假设它比上述循环中的2000-10,000,000次迭代更快)。 - Brian Webster

    1
    你不需要循环,计算从0001年1月1日到Unix纪元开始(1970年1月1日00:00:00)的秒数,并将其保存在某个地方。然后从输入中减去它,再使用任何可用的工具将Unix时间戳(从1970年1月1日起的秒数)转换为年份,然后加上1970年。我不太了解VB编程,无法提供详细指南。

    1
    Const TICKS_PER_YEAR As Long = 315360000000000
    Function YearsSinceBeginningOfTimeUntil(ByVal d As DateTime) As Integer
        Return Math.Floor(d.Ticks / TICKS_PER_YEAR)
    End Function
    

    1
    每年的时钟周期接近于31556952,略高于非闰年的时钟周期,因此我们刚好无法达到下一个整数,因此Floor函数会给出上一年的年份。 - Brian Webster

    0

    我知道这个问题现在已经很老了,但我经常看到类似的问题,并且这里没有任何简单的答案。

    我的解决方案使用了一个古老的技巧,将两个日期写成数字形式(例如,“2013年12月12日”写成20131212),然后相减并丢弃最后四位数字。我挖出了我的F#实现,您可以将其粘贴到LinqPad中以检查答案。它也考虑了闰年等因素:

    let dateFrom = new DateTime(1,1,1)
    
    let dateTo = dateFrom.AddSeconds(100000000.0)
    
    let yearsSince dateFrom dateTo =
        let intRepresentation (date: DateTime) = 
            date.ToString "yyyy.MMdd" |> Convert.ToDouble
    
        let dateToNum = intRepresentation dateTo
        let dateFromNum = intRepresentation dateFrom
    
        int (dateToNum - dateFromNum)
    
    yearsSince dateFrom dateTo |> Dump
    
    let dob = DateTime(1985, 4, 16)
    
    let calculateAge = yearsSince dob
    
    calculateAge DateTime.Today |> Dump
    

    请注意,这相当简单:它没有考虑到除.NET的DateTime类已处理的时区或历史时区更改之外的任何时区或历史时区更改。实际的繁重工作是由DateTime.AddSeconds方法执行的。希望这可以帮到你。

    0

    以下假设公历将在未来的五百八十四亿五千万年内继续有效。但是要做好失望的准备;随着我们的太阳开始膨胀,改变地球的轨道和一年的长度,日历可能会被废除,并且当地球在七十五亿年后坠入太阳时,很可能会采用其他措施。

    顺便说一下,我甚至不尝试处理格里高利历之前的日期。我只返回日期在1582年10月15日之前发生的天数,需要能够表达这种返回值的原因是GetDateFromSerial函数具有asString参数。

    Sub GetDateFromSerial(ByVal dateSerial As ULong, ByRef year As Long, ByRef month As Integer, ByRef dayOfMonth As Integer, ByRef secondsIntoDay As Integer, ByRef asString As String)
        Const SecondsInOneDay As ULong = 86400 ' 24 hours * 60 minutes per hour * 60 seconds per minute
    
        'Dim startOfGregorianCalendar As DateTime = New DateTime(1582, 10, 15)
        'Dim startOfGregorianCalendarInSeconds As ULong = (startOfGregorianCalendar - New DateTime(1, 1, 1)).TotalSeconds
    
        Const StartOfGregorianCalendarInSeconds As ULong = 49916304000
    
        secondsIntoDay = dateSerial Mod SecondsInOneDay
    
        If dateSerial < StartOfGregorianCalendarInSeconds Then
            year = -1
            month = -1
            dayOfMonth = -1
    
            Dim days As Integer = (StartOfGregorianCalendarInSeconds - dateSerial) \ SecondsInOneDay
    
            asString = days & IIf(days = 1, " day", " days") & " before the adoption of the Gregorian calendar on October 15, 1582"
        Else
            'Dim maximumDateValueInSeconds As ULong = (DateTime.MaxValue - New DateTime(1, 1, 1)).TotalSeconds
            Const MaximumDateValueInSeconds As ULong = 315537897600
    
            If dateSerial <= MaximumDateValueInSeconds Then
                Dim parsedDate As DateTime = DateTime.MinValue.AddSeconds(dateSerial)
    
                year = parsedDate.Year
                month = parsedDate.Month
                dayOfMonth = parsedDate.Day
            Else
                ' Move the date back into the range that DateTime can parse, by stripping away blocks of
                ' 400 years. Aim to put the date within the range of years 2001 to 2400.
                Dim dateSerialInDays As ULong = dateSerial \ SecondsInOneDay
    
                Const DaysInFourHundredYears As Integer = 365 * 400 + 97 ' Three multiple-of-4 years in each 400 are not leap years.
    
                Dim fourHundredYearBlocks As Integer = dateSerialInDays \ DaysInFourHundredYears
    
                Dim blocksToFactorInLater As Integer = fourHundredYearBlocks - 5
    
                Dim translatedDateSerialInDays As ULong = dateSerialInDays - blocksToFactorInLater * CLng(DaysInFourHundredYears)
    
                ' Parse the date as normal now.
                Dim parsedDate As DateTime = DateTime.MinValue.AddDays(translatedDateSerialInDays)
    
                year = parsedDate.Year
                month = parsedDate.Month
                dayOfMonth = parsedDate.Day
    
                ' Factor back in the years we took out earlier.
                year += blocksToFactorInLater * 400L
            End If
    
            asString = New DateTime(2000, month, dayOfMonth).ToString("dd MMM") & ", " & year
        End If
    End Sub
    
    Function GetSerialFromDate(ByVal year As Long, ByVal month As Integer, ByVal dayOfMonth As Integer, ByVal secondsIntoDay As Integer) As ULong
        Const SecondsInOneDay As Integer = 86400 ' 24 hours * 60 minutes per hour * 60 seconds per minute
    
        If (year < 1582) Or _
           ((year = 1582) And (month < 10)) Or _
           ((year = 1582) And (month = 10) And (dayOfMonth < 15)) Then
            Throw New Exception("The specified date value has no meaning because it falls before the point at which the Gregorian calendar was adopted.")
        End If
    
        ' Use DateTime for what we can -- which is years prior to 9999 -- and then factor the remaining years
        ' in. We do this by translating the date back by blocks of 400 years (which are always the same length,
        ' even factoring in leap years), and then factoring them back in after the fact.
    
        Dim fourHundredYearBlocks As Integer = year \ 400
    
        Dim blocksToFactorInLater As Integer = fourHundredYearBlocks - 5
    
        If blocksToFactorInLater < 0 Then blocksToFactorInLater = 0
    
        year = year - blocksToFactorInLater * 400L
    
        Dim dateValue As DateTime = New DateTime(year, month, dayOfMonth)
    
        Dim translatedDateSerialInDays As ULong = (dateValue - New DateTime(1, 1, 1)).TotalDays
    
        Const DaysInFourHundredYears As ULong = 365 * 400 + 97 ' Three multiple-of-4 years in each 400 are not leap years.
    
        Dim dateSerialInDays As ULong = translatedDateSerialInDays + blocksToFactorInLater * DaysInFourHundredYears
    
        Dim dateSerial As ULong = dateSerialInDays * SecondsInOneDay + secondsIntoDay
    
        Return dateSerial
    End Function
    

    -2

    我认为这对你会有帮助:

    function foo(days):
      count = days
      year = 0
      while (count > 0):
        if leap_year(year)
          count = count - 366
        else
          count = count - 365
        year ++
      return year
    

    4
    我认为一年不止有 366 秒。 - BlueRaja - Danny Pflughoeft
    这不是原帖所要求的。 - ReinstateMonica3167040

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接