8

Input: # of seconds since January 1st, of Year 0001

Output: # of Full years during this time period

I have developed an algorithm that I do not think is the optimal solution. I think there should be a solution that does not involve a loop. See Code Block 1 for the algorithm which A) Determines the quantity of days and B) Iteratively subtracts 366 or 365 depending on Leap Years from the Day Total while incrementing the Year Total

It's not as simple as Dividing DayCount by 365.2425 and truncating, because we hit a failure point at on January 1, 0002 (31536000 Seconds / (365.2425 * 24 * 60 * 60)) = 0.99934.

Any idea on a non-looping method for extracting years from a quantity of seconds since January 1, 0001 12:00 AM?

I need to figure this out because I need a date embedded in a long (which stores seconds) so that I can track years out to 12+ million with 1-second precision.

Code block 1 - Inefficient Algorithm to get Years from Seconds (Including Leap Years)

        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

Edit: My solution was to skip the memory savings of embedding a date within 8 bits and to store each value (seconds through years) in separate integers. This causes instant retrievals at the expense of memory.

Edit2: Typo in first edit (8bits)

Brian Webster
  • 30,033
  • 48
  • 152
  • 225
  • 5
    You are aware of calendar reforms that resulted in a few days having been skipped? – Thilo Jan 27 '10 at 06:00
  • 2
    Don't forget leap seconds! :D – Miles Jan 27 '10 at 06:52
  • I'm confused, why aren't you using the epoch? – JSchlather Jan 27 '10 at 07:15
  • @Liberalkid - Not sure what you're referring to, but I need 1-second precision out to 12 million years embedded within 8 bytes. – Brian Webster Jan 27 '10 at 08:21
  • @Thilo - That's a good point, though for the purposes of this structure it won't matter. I could start it from 1900 and be more technically correct. Even leap seconds won't make any difference. The main point is that I need to encode to 8-bytes (long) and decode and have the same value (and handle leap years w/ Feb 29th). This is rather trivial to calculate with a loop, but I'd really rather avoid between 1 and 12 million iterations through a loop just to decode a year. Clearly I may be trying to trade too much processing power for too little storage – Brian Webster Jan 27 '10 at 08:24

7 Answers7

25

If you need accuracy to the very second, you'll want a commercial-grade datetime package; it's just too complex to do accurately with a simple algorithm. For instance:

Because of these and more complications, you are better off not writing the code yourself, unless you can relax the constraint that you need accuracy to the very second over 12-million years.

"October 4, 1582 – Saint Teresa of Ávila dies. She is buried the next day, October 15."

BlueRaja - Danny Pflughoeft
  • 84,206
  • 33
  • 197
  • 283
5

Wikipeda has an article on Julian Date with an algorithm which you could adapt to your needs.

Carlos Gutiérrez
  • 13,972
  • 5
  • 37
  • 47
  • This may work, I'll look into it a bit more. If I can get a O(1) algorithm to go Seconds since 1900 -> Julian -> Gregorian, then we'll be set (assuming it's quicker than 2000-10,000,000 iterations through the above loop) – Brian Webster Jan 27 '10 at 08:18
1

You don't need a loop, calculate seconds from 1 Jan 0001 to unix epoch start (1 Jan 1970 00:00:00), and save somewhere. Then subtract it from your input, then use any tool available to convert unix timestamp (seconds from 1 Jan 1970) to years, then add 1970. I don't know much VB programming to post a detailed guide.

Luka Ramishvili
  • 889
  • 1
  • 11
  • 20
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
Jay
  • 56,361
  • 10
  • 99
  • 123
  • 1
    Ticks per year is closer to 31556952 which is slightly more than the ticks per non leapyear, so we fall just short of achieving the next integer, thus Floor gives us the previous year number. – Brian Webster Jan 27 '10 at 08:20
0

I know this question is old now, but I see ones like it often and there weren't any easy answers in here.

My solution uses an old trick of writing two dates as if they were numbers (e.g. 'Dec 12th 2013' as 20131212) and then subtracting one from the other and discarding the last four digits. I dug up my implementation in F#, you can paste it into LinqPad to check answers. Takes leap years etc into account too:

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

Please note that this is quite naive: it doesn't take timezones or historical timezone changes into account beyond those that are already handled by .NET's DateTime class. The actual grunt work is being done by the DateTime.AddSeconds method. Hope this helps.

Richiban
  • 5,569
  • 3
  • 30
  • 42
0

The following assumes that the Gregorian calendar will remain in effect for the upcoming five hundred and eighty four and a half billion years. Be prepared for disappointment, though; the calendar may end up being scrapped as our sun begins to expand, changing the orbit of the Earth and the duration of the year, and it is very likely something else will be adopted when the Earth falls into the sun seven and a half billion years from now.

As an aside, I don't even try to handle dates prior to the adoption of the Gregorian calendar. I simply return the number of days the date occurred prior to the 15th of October, 1582, and the need to be able to express that sort of return value is the only reason the GetDateFromSerial function has an asString parameter.

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
Jonathan Gilbert
  • 3,526
  • 20
  • 28
-2

I think that this will work for you:

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