0

I have a spreadsheet that includes a "Last Modified Date" in epoch format in column C, such as:

1486841495 (earliest date in 2017) 1574478516 (latest date in 2019)

Column C has 6,003 rows. What I want to do is have a script take what's in column C and convert it to the following format in column E: MM/DD/YY HH:MM:SS AM/PM. I have the result cells formatted correctly so that's showing up right. I'm in Central time zone.

I have very little exposure to writing code and I've been trying to piece something together for hours.

This is the closest I've been able to get, but it only runs on one cell and I need it to run on the entire column C. Can anyone help?

Sub CalcDate()

'2017

If Range("C2").Value > 1483250400 And Range("C2").Value < 1489298520 Then
Range("E2:E10").Value = ((Range("C2:E10") - 21600) / 86400) + 25569

ElseIf Range("C2").Value > 1489298520 And Range("C2").Value < 1509858120 Then
Range("E2").Value = ((Range("C2") - 18000) / 86400) + 25569

ElseIf Range("C2").Value > 1509858120 And Range("C2").Value < 1514743199 Then
Range("E2").Value = ((Range("C2") - 21600) / 86400) + 25569

'2018

ElseIf Range("C2").Value > 1514786400 And Range("C2").Value < 1520755200 Then
Range("E2:E10").Value = ((Range("C2:E10") - 21600) / 86400) + 25569

ElseIf Range("C2").Value > 1520755200 And Range("C2").Value < 1541318400 Then
Range("E2").Value = ((Range("C2") - 18000) / 86400) + 25569

ElseIf Range("C2").Value > 1541318400 And Range("C2").Value < 1546279199 Then
Range("E2").Value = ((Range("C2") - 21600) / 86400) + 25569

'2019

 ElseIf Range("C2").Value > 1546322400 And Range("C2").Value < 1552204800 Then
    Range("E2:E10").Value = ((Range("C2:E10") - 21600) / 86400) + 25569

    ElseIf Range("C2").Value > 1552204800 And Range("C2").Value < 1572768000 Then
    Range("E2").Value = ((Range("C2") - 18000) / 86400) + 25569

    ElseIf Range("C2").Value > 1572768000 And Range("C2").Value < 1577815199 Then
    Range("E2").Value = ((Range("C2") - 21600) / 86400) + 25569


End If
End Sub
braX
  • 11,506
  • 5
  • 20
  • 33
wtwenz
  • 23
  • 4
  • 1
    https://stackoverflow.com/questions/12325948/excel-vba-convert-a-date-string-to-a-unix-timestamp/12326121#12326121 and format your result cells with the appropriate date format. – Tim Williams Dec 05 '19 at 21:00
  • 1
    ...might need to add a timezone offset if your timestamps are UTC https://en.wikipedia.org/wiki/Unix_time – Tim Williams Dec 05 '19 at 21:06
  • Thank you. I do have the result cells correctly formatted and the timezone offset is included in my formula. How do I expand the range to include column C? – wtwenz Dec 05 '19 at 21:14
  • Using the code in the linked question: in E2 put `=fromUnix(C2)` and fill down – Tim Williams Dec 05 '19 at 23:15

2 Answers2

1

I think the "=fromUnix(C2)" formula suggested by Tim Williams in the question's comments may be the most straightforward way to resolve the conversion of all the epoch values to UTC. But, if the intent is to learn how to do it with VBA alone, including the conversion to local time, the code below will hopefully be of some help to someone.

NB: An advantage to having a VBA macro is that it can write out all the results in one fell swoop and avoids the need to add 6,000 new formulas in the workbook. The disadvantage is that the conversion is not dynamic, so any changes in the epoch values will require running the macro again.

Tactic

We would need a procedure that reads in the epoch values from one column into an array, converts each value in the array from epoch to local time, and then prints that array to a different column. To perform the conversion of each individual epoch value, we must first convert it to UTC, and from UTC to local time.

To do the 2nd conversion, we will need to rely on some code adapted from an answer posted by ashleedawg at Convert UTC time to local. The nice thing about that code is that it takes into account the DST status at the time of the UTC value, as opposed to the current DST status at the time of the calculation. This was not the case in the Chip Pearson code (at this link) that I had previously suggested for this answer.

Code modules to add

We would need to create a new VBA code module with the following adaptation of ashleedawg's code:

Option Explicit
'Adapted from ashleedawg's answer at
'  https://stackoverflow.com/questions/23903872/convert-utc-time-to-local#50452663
'That answer, in turn, was adapted from code by Tim Hall at
'  https://github.com/VBA-tools/VBA-UTC

'PUBLIC FUNCTIONS:
'    - UTCtoLocal(utc_UtcDate As Date) As Date     converts UTC datetimes to local
'    - LocalToUTC(utc_LocalDate As Date) As Date   converts local DateTime to UTC
'Accuracy confirmed for several variations of time zones & DST rules. (ashleedawg)
'===============================================================================

Private Type utc_SYSTEMTIME
    utc_wYear As Integer: utc_wMonth As Integer: utc_wDayOfWeek As Integer: utc_wDay As Integer
    utc_wHour As Integer: utc_wMinute As Integer: utc_wSecond As Integer: utc_wMilliseconds As Integer
End Type

Private Type utc_TIME_ZONE_INFORMATION
    utc_Bias As Long: utc_StandardName(0 To 31) As Integer: utc_StandardDate As utc_SYSTEMTIME: utc_StandardBias As Long
    utc_DaylightName(0 To 31) As Integer: utc_DaylightDate As utc_SYSTEMTIME: utc_DaylightBias As Long
End Type

'http://msdn.microsoft.com/library/windows/desktop/ms724421.aspx /ms724949.aspx /ms725485.aspx
#If VBA7 Then
Private Declare PtrSafe Function utc_GetTimeZoneInformation Lib "kernel32" Alias "GetTimeZoneInformation" _
    (utc_lpTimeZoneInformation As utc_TIME_ZONE_INFORMATION) As Long
Private Declare PtrSafe Function utc_SystemTimeToTzSpecificLocalTime Lib "kernel32" Alias "SystemTimeToTzSpecificLocalTime" _
    (utc_lpTimeZoneInformation As utc_TIME_ZONE_INFORMATION, utc_lpUniversalTime As utc_SYSTEMTIME, utc_lpLocalTime As utc_SYSTEMTIME) As Long
Private Declare PtrSafe Function utc_TzSpecificLocalTimeToSystemTime Lib "kernel32" Alias "TzSpecificLocalTimeToSystemTime" _
    (utc_lpTimeZoneInformation As utc_TIME_ZONE_INFORMATION, utc_lpLocalTime As utc_SYSTEMTIME, utc_lpUniversalTime As utc_SYSTEMTIME) As Long
#Else
Private Declare Function utc_GetTimeZoneInformation Lib "kernel32" Alias "GetTimeZoneInformation" _
    (utc_lpTimeZoneInformation As utc_TIME_ZONE_INFORMATION) As Long
Private Declare Function utc_SystemTimeToTzSpecificLocalTime Lib "kernel32" Alias "SystemTimeToTzSpecificLocalTime" _
    (utc_lpTimeZoneInformation As utc_TIME_ZONE_INFORMATION, utc_lpUniversalTime As utc_SYSTEMTIME, utc_lpLocalTime As utc_SYSTEMTIME) As Long
Private Declare Function utc_TzSpecificLocalTimeToSystemTime Lib "kernel32" Alias "TzSpecificLocalTimeToSystemTime" _
    (utc_lpTimeZoneInformation As utc_TIME_ZONE_INFORMATION, utc_lpLocalTime As utc_SYSTEMTIME, utc_lpUniversalTime As utc_SYSTEMTIME) As Long
#End If

Private Function utc_DateToSystemTime(utc_Value As Date) As utc_SYSTEMTIME ' "Helper Function" for Public subs (below)
    With utc_DateToSystemTime
        .utc_wYear = Year(utc_Value): .utc_wMonth = Month(utc_Value): .utc_wDay = Day(utc_Value)
        .utc_wHour = Hour(utc_Value): .utc_wMinute = Minute(utc_Value): .utc_wSecond = Second(utc_Value): .utc_wMilliseconds = 0
    End With
End Function

Private Function utc_SystemTimeToDate(utc_Value As utc_SYSTEMTIME) As Date ' "Helper Function" for Public Functions (below)
    utc_SystemTimeToDate = DateSerial(utc_Value.utc_wYear, utc_Value.utc_wMonth, utc_Value.utc_wDay) + _
        TimeSerial(utc_Value.utc_wHour, utc_Value.utc_wMinute, utc_Value.utc_wSecond)
End Function

'===============================================================================

Public Function UTCtoLocal(utc_UtcDate As Date) As Date
    On Error GoTo errorUTC
    Dim utc_TimeZoneInfo As utc_TIME_ZONE_INFORMATION, utc_LocalDate As utc_SYSTEMTIME
    utc_GetTimeZoneInformation utc_TimeZoneInfo
    utc_SystemTimeToTzSpecificLocalTime utc_TimeZoneInfo, utc_DateToSystemTime(utc_UtcDate), utc_LocalDate
    UTCtoLocal = utc_SystemTimeToDate(utc_LocalDate)
    Exit Function
errorUTC:
    Debug.Print "UTC parsing error: " & Err.Number & " - " & Err.Description: Stop
End Function

Public Function LocalToUTC(utc_LocalDate As Date) As Date
    On Error GoTo errorUTC
    Dim utc_TimeZoneInfo As utc_TIME_ZONE_INFORMATION, utc_UtcDate As utc_SYSTEMTIME
    utc_GetTimeZoneInformation utc_TimeZoneInfo
    utc_TzSpecificLocalTimeToSystemTime utc_TimeZoneInfo, utc_DateToSystemTime(utc_LocalDate), utc_UtcDate
    LocalToUTC = utc_SystemTimeToDate(utc_UtcDate)
    Exit Function
errorUTC:
    Debug.Print "UTC conversion error: " & Err.Number & " - " & Err.Description: Stop
End Function

After that, we can create a second module with the following code to convert from epoch seconds to local time:

Option Explicit

Function EpochToLocal(ByVal unixSecs As Variant) As Variant
  EpochToLocal = "" 'Default value returned in case the conversion is not possible
  On Error Resume Next
  If IsNumeric(unixSecs & "") Then EpochToLocal = UTCtoLocal(EpochToUTC(CLng(unixSecs)))
  'UTCtoLocal is adapted from the answer posted by ashleedawg at
  '  https://stackoverflow.com/questions/23903872/convert-utc-time-to-local#50452663
End Function

Function EpochToUTC(ByVal unixSecs As Long)
  EpochToUTC = DateAdd("s", unixSecs, #1/1/1970#) 
End Function

Then, we can create a third VBA module and paste the following macro to do the actual conversion:

Option Explicit

Sub ConvertAllUnixTimestamps()
  'This range must be set to the 1-column block of cells with all the epoch-seconds values;
  '  to simplify the code, this method assumes that this range has at least 2 rows
  'The range is being hard-coded here, but it could be passed as a parameter in future
  Dim epochColumn As Range: Set epochColumn = Range("C2:C6003")

  'This range must be set to the first cell where the local times must be written
  'The cell is being hard-coded here, but it could be passed as a parameter in future
  Dim localFirstCell As Range: Set localFirstCell = Range("E2")

  'Read in all the epoch values into a 2-dimensional array (assuming the range has 2+ rows)
  Dim epochArr As Variant: epochArr = epochColumn.value

  'Get the lower and upper bounds of the array's 1st dimension with the epoch values
  Dim epochLb As Long: epochLb = LBound(epochArr, 1)
  Dim epochUb As Long: epochUb = UBound(epochArr, 1)
  'Get the lower bound of the array's 2nd dimension;
  '  since we only care about the 1st column of values, the 2nd dim's upper bound is not used
  Dim index2 As Long: index2 = LBound(epochArr, 2)
  'Get the number of epoch values to process
  Dim epochCount As Long: epochCount = epochUb - epochLb + 1

  'Convert all the values in the array from epoch seconds to local times
  Dim i As Long
  For i = epochLb To epochUb
    epochArr(i, index2) = EpochToLocal(epochArr(i, index2))
  Next

  'Create a range that goes from the first cell and goes down [epochCount] rows,
  '  and then write the converted array to it
  localFirstCell.Resize(epochCount).value = epochArr
End Sub

Subtracting leap seconds for a more exact UTC conversion

I believe that, for most cases, the above code would do. However, the Adrian Monk's of the world may disagree. This is because the number of seconds in epoch values are not necessarily aligned with UTC. As explained on Wikipedia, epoch timestamps may not take into account the pesky "leap seconds" that are occasionally inserted into UTC time to account for astronomical variations delaying the earth's rotation. If those precious seconds need to be accounted for, the second module mentioned above, the one with the EpochToLocal function, would need to be replaced with something like the code below:

NB: If the epoch values being converted are from a clock that is counting the actual seconds since 1 Jan 1970 (e.g. a TAI-based clock), then adding the leap seconds during the conversion is theoretically necessary. However, if the epoch values are a mere "formatting" of UTC times that simply subtracted 1 Jan 1970 from the UTC time, then leap seconds should not be removed because they have already been accounted for.

Option Explicit

Dim LeapSecDates() As Variant 'Array to store all the dates when leap secs. were added to the UTC
Dim LeapSecLb As Long, LeapSecUb As Long 'Bounds of the leap-seconds array
Dim LeapSecDatesLoaded As Boolean 'Indicates whether the leap-seconds array has been loaded yet

Function EpochToLocal(ByVal unixSecs As Variant) As Variant
  EpochToLocal = "" 'Default value returned in case the conversion is not possible
  On Error Resume Next
  If IsNumeric(unixSecs & "") Then EpochToLocal = UTCtoLocal(EpochToUTC(CLng(unixSecs)))
  'UTCtoLocal is adapted from the answer posted by ashleedawg at
  '  https://stackoverflow.com/questions/23903872/convert-utc-time-to-local#50452663
End Function

Function EpochToUTC(ByVal unixSecs As Long)
  Dim dte As Date
  dte = DateAdd("s", unixSecs, #1/1/1970#) 'This takes us to UTC, but w/ extra leap secs.
  dte = DateAdd("s", -LeapSecondsFor(dte), dte) 'Removing the extra leap seconds

  EpochToUTC = dte
End Function

Private Function LeapSecondsFor(ByVal dte As Date)
  Dim i As Long

  If Not LeapSecDatesLoaded Then 'To save time, the leap-seconds array is only loaded once
    'Based on table at https://en.wikipedia.org/wiki/Leap_second#Insertion_of_leap_seconds
    '  as of Dec 2019; the dates must be in ascending order
    LeapSecDates = Array(#6/30/1972#, #12/31/1972#, #12/31/1973#, #12/31/1974# _
      , #12/31/1975#, #12/31/1976#, #12/31/1977#, #12/31/1978#, #12/31/1979# _
      , #6/30/1981#, #6/30/1982#, #6/30/1983#, #6/30/1985#, #12/31/1987#, #12/31/1989# _
      , #12/31/1990#, #6/30/1992#, #6/30/1993#, #6/30/1994#, #12/31/1995# _
      , #6/30/1997#, #12/31/1998#, #12/31/2005#, #12/31/2008#, #7/31/2012# _
      , #6/30/2015#, #12/31/2016#) 'This array needs to be manually updated as new dates emerge
    LeapSecLb = LBound(LeapSecDates)
    LeapSecUb = UBound(LeapSecDates)

    'Move the time to midnight of the next day for each date in the array
    For i = LeapSecLb To LeapSecUb
      LeapSecDates(i) = DateAdd("d", 1, LeapSecDates(i))
    Next

    LeapSecDatesLoaded = True
  End If

  'Get the number of leap-second dates that have elapsed up until the date [dte];
  '  e.g. if [dte] - 23 secs. is > the last 24 dates in the array, then 24 leap seconds
  '       must be removed
  Dim leap As Long: leap = 0
  Dim previousLeapSecs As Long
  For i = LeapSecUb To LeapSecLb Step -1
    previousLeapSecs = i - LeapSecLb
    If DateAdd("s", -previousLeapSecs, dte) > LeapSecDates(i) Then
      LeapSecondsFor = previousLeapSecs + 1
      Exit Function
    End If
  Next

  'If we are here, no leap seconds were added before [dte]
  LeapSecondsFor = 0
End Function

Do bear in mind that the modules above have many extra comments that make the code look lengthier than it really is. If you do not need all the play-by-play, you can delete many of the comments to get a better sense of what the code tries to do.

Noah Bridge
  • 345
  • 2
  • 6
  • I am attempting to use this solution, but it seems to use my current time zone to convert the dates in my spreadsheet. I need it to calculate the date/time based on what timezone it was then. All dates that occurred outside of daylight savings time are correct (I think because I'm not currently in DST), but all dates that occurred during daylight savings time are one hour too early. – wtwenz Dec 10 '19 at 16:51
  • @wtwenz, you are right. I had not realised that Chip Pearson's code was checking the DST status at calculation time, as opposed to the DST status at the time of the UTC value. I found some code that addresses that issue, and I pasted an adaptation of it in the answer (instead of just linking to it, as before). You can delete the module with the Chip Pearson code and replace it with this new code. After that, you will need to re-copy/paste the "EpochToLocal" function (the only change was to call the new "UTCToLocal" function instead of the old "GetLocalTimeFromGMT" function, plus a new comment) – Noah Bridge Dec 10 '19 at 19:44
  • This revised solution that you posted worked perfectly and I cannot thank you enough!! I appreciate your time providing a well explained and thorough answer and also the follow up! – wtwenz Dec 10 '19 at 20:26
  • That's great to know :) Thanks for the update, @wtwenz. I am glad all the combined code worked! – Noah Bridge Dec 11 '19 at 01:27
0

You need a For loop:

Sub CalcDate()
    Dim cell as Variant

    For Each cell in Range("C2:C6003")
    '2017
    If cell.Value > 1483250400 And cell.Value < 1489298520 Then
         Range(Cells(cell.Row, 3),Cells(cell.Row,5)).Value = ((Range(cell,"E10") - 21600) / 86400) + 25569 
         'Not sure if E10 depends on the C2 value, but use Cells(cell.row,column number) to get some
          sort of logic to connect it back to the location of cell.
         'blah blah blah
    '2018
         'blah blah blah
    '2019 
         'blah blah blah
    End If
    Next cell

End Sub
PuravTheGreat
  • 136
  • 12
  • In my situation, E2 depends on C2, E3 depends on C3, and so on and so forth for the entire spreadsheet. Still not quite sure how to write the line beginning with Range(Cells(cell.Row, 3). If you could help me with that one line, I should be able to extrapolate the rest. – wtwenz Dec 10 '19 at 17:08
  • `Range(Cells(cell.Row,3),Cells(cell.Row,5)).Value` is how you'd write it. `cell.Row` returns the row of the variable `cell` from the `For` loop, so in the first case it would be C2, then C3, and `cell.Row` returns the row so for C2 it'd be 2, for C3 it'd be 3, etc... – PuravTheGreat Dec 11 '19 at 06:37