在 Excel 中解析 ISO8601 日期/时间(包括 TimeZone)
Posted
技术标签:
【中文标题】在 Excel 中解析 ISO8601 日期/时间(包括 TimeZone)【英文标题】:Parsing an ISO8601 date/time (including TimeZone) in Excel 【发布时间】:2011-06-21 05:36:39 【问题描述】:我需要将 Excel/VBA 中包含时区(来自外部源)的 ISO8601 日期/时间格式解析为普通的 Excel 日期。据我所知,Excel XP(我们正在使用的)没有内置的例程,所以我想我正在寻找一个用于解析的自定义 VBA 函数。
ISO8601 日期时间类似于以下之一:
2011-01-01
2011-01-01T12:00:00Z
2011-01-01T12:00:00+05:00
2011-01-01T12:00:00-05:00
2011-01-01T12:00:00.05381+05:00
【问题讨论】:
现在是 2020 年,通过 Office 365 的最新版 Excel仍然在其庞大的公式库中没有简单的TryParseExactDate( "yyyy-MM-dd'T'HH:mm:ss", A1 )
函数。微软的借口是什么? :(
【参考方案1】:
有一种(相当)简单的方法可以使用公式而不是宏来解析没有时区的 ISO 时间戳。这不是完全原发帖人所问的,但我在尝试在 Excel 中解析 ISO 时间戳时发现了这个问题,发现 this solution 很有用,所以我想我会在这里分享。
下面的公式将解析一个 ISO 时间戳,同样没有时区:
=DATEVALUE(MID(A1,1,10))+TIMEVALUE(MID(A1,12,8))
这将生成浮点格式的日期,然后您可以使用普通 Excel 格式将其格式化为日期。
【讨论】:
但是这个方案没有考虑时区转换。 如果时区不相关或完全相同,例如本地时区,这是一个合理的选择。 对于其他任何搜索将浮点数转换为日期但一无所获的人 - 一旦采用这种格式42482.00
,您可以简单地将其格式化为日期或使用 =year(...)
或 =DATE(YEAR(G1), MONTH(G1), DAY(G1))
如果您需要并且您的输入包含它,您可以将 8
更改为 12
以包含毫秒。
我用这个来转换时间码。只需将 HH:MM 差异放在最后一部分,然后根据时区进行加减。就我而言,我落后了 6 小时,所以我减去它。 =DATEVALUE(MID(C2,1,10))+TIMEVALUE(MID(C2,12,8))-TIMEVALUE("6:00")
【参考方案2】:
很多谷歌搜索都没有找到任何东西,所以我编写了自己的例程。在这里发布以供将来参考:
Option Explicit
'---------------------------------------------------------------------
' Declarations must be at the top -- see below
'---------------------------------------------------------------------
Public Declare Function SystemTimeToFileTime Lib _
"kernel32" (lpSystemTime As SYSTEMTIME, _
lpFileTime As FILETIME) As Long
Public Declare Function FileTimeToLocalFileTime Lib _
"kernel32" (lpLocalFileTime As FILETIME, _
lpFileTime As FILETIME) As Long
Public Declare Function FileTimeToSystemTime Lib _
"kernel32" (lpFileTime As FILETIME, lpSystemTime _
As SYSTEMTIME) As Long
Public Type FILETIME
dwLowDateTime As Long
dwHighDateTime As Long
End Type
Public Type SYSTEMTIME
wYear As Integer
wMonth As Integer
wDayOfWeek As Integer
wDay As Integer
wHour As Integer
wMinute As Integer
wSecond As Integer
wMilliseconds As Integer
End Type
'---------------------------------------------------------------------
' Convert ISO8601 dateTimes to Excel Dates
'---------------------------------------------------------------------
Public Function ISODATE(iso As String)
' Find location of delimiters in input string
Dim tPos As Integer: tPos = InStr(iso, "T")
If tPos = 0 Then tPos = Len(iso) + 1
Dim zPos As Integer: zPos = InStr(iso, "Z")
If zPos = 0 Then zPos = InStr(iso, "+")
If zPos = 0 Then zPos = InStr(tPos, iso, "-")
If zPos = 0 Then zPos = Len(iso) + 1
If zPos = tPos Then zPos = tPos + 1
' Get the relevant parts out
Dim datePart As String: datePart = Mid(iso, 1, tPos - 1)
Dim timePart As String: timePart = Mid(iso, tPos + 1, zPos - tPos - 1)
Dim dotPos As Integer: dotPos = InStr(timePart, ".")
If dotPos = 0 Then dotPos = Len(timePart) + 1
timePart = Left(timePart, dotPos - 1)
' Have them parsed separately by Excel
Dim d As Date: d = DateValue(datePart)
Dim t As Date: If timePart <> "" Then t = TimeValue(timePart)
Dim dt As Date: dt = d + t
' Add the timezone
Dim tz As String: tz = Mid(iso, zPos)
If tz <> "" And Left(tz, 1) <> "Z" Then
Dim colonPos As Integer: colonPos = InStr(tz, ":")
If colonPos = 0 Then colonPos = Len(tz) + 1
Dim minutes As Integer: minutes = CInt(Mid(tz, 2, colonPos - 2)) * 60 + CInt(Mid(tz, colonPos + 1))
If Left(tz, 1) = "+" Then minutes = -minutes
dt = DateAdd("n", minutes, dt)
End If
' Return value is the ISO8601 date in the local time zone
dt = UTCToLocalTime(dt)
ISODATE = dt
End Function
'---------------------------------------------------------------------
' Got this function to convert local date to UTC date from
' http://excel.tips.net/Pages/T002185_Automatically_Converting_to_GMT.html
'---------------------------------------------------------------------
Public Function UTCToLocalTime(dteTime As Date) As Date
Dim infile As FILETIME
Dim outfile As FILETIME
Dim insys As SYSTEMTIME
Dim outsys As SYSTEMTIME
insys.wYear = CInt(Year(dteTime))
insys.wMonth = CInt(Month(dteTime))
insys.wDay = CInt(Day(dteTime))
insys.wHour = CInt(Hour(dteTime))
insys.wMinute = CInt(Minute(dteTime))
insys.wSecond = CInt(Second(dteTime))
Call SystemTimeToFileTime(insys, infile)
Call FileTimeToLocalFileTime(infile, outfile)
Call FileTimeToSystemTime(outfile, outsys)
UTCToLocalTime = CDate(outsys.wMonth & "/" & _
outsys.wDay & "/" & _
outsys.wYear & " " & _
outsys.wHour & ":" & _
outsys.wMinute & ":" & _
outsys.wSecond)
End Function
'---------------------------------------------------------------------
' Tests for the ISO Date functions
'---------------------------------------------------------------------
Public Sub ISODateTest()
' [[ Verify that all dateTime formats parse sucesfully ]]
Dim d1 As Date: d1 = ISODATE("2011-01-01")
Dim d2 As Date: d2 = ISODATE("2011-01-01T00:00:00")
Dim d3 As Date: d3 = ISODATE("2011-01-01T00:00:00Z")
Dim d4 As Date: d4 = ISODATE("2011-01-01T12:00:00Z")
Dim d5 As Date: d5 = ISODATE("2011-01-01T12:00:00+05:00")
Dim d6 As Date: d6 = ISODATE("2011-01-01T12:00:00-05:00")
Dim d7 As Date: d7 = ISODATE("2011-01-01T12:00:00.05381+05:00")
AssertEqual "Date and midnight", d1, d2
AssertEqual "With and without Z", d2, d3
AssertEqual "With timezone", -5, DateDiff("h", d4, d5)
AssertEqual "Timezone Difference", 10, DateDiff("h", d5, d6)
AssertEqual "Ignore subsecond", d5, d7
' [[ Independence of local DST ]]
' Verify that a date in winter and a date in summer parse to the same Hour value
Dim w As Date: w = ISODATE("2010-02-23T21:04:48+01:00")
Dim s As Date: s = ISODATE("2010-07-23T21:04:48+01:00")
AssertEqual "Winter/Summer hours", Hour(w), Hour(s)
MsgBox "All tests passed succesfully!"
End Sub
Sub AssertEqual(name, x, y)
If x <> y Then Err.Raise 1234, Description:="Failed: " & name & ": '" & x & "' <> '" & y & "'"
End Sub
【讨论】:
必须在我的系统上的每个Declare
之前添加 PtrSafe
。
是的,这不起作用。如果您添加一个测试Dim d8 As Date: d8 = ISODATE("2020-01-02T16:46:00")
,它是 1 月 2 日的有效 ISO 日期,它会返回 2 月 1 日...您的测试非常乐观。【参考方案3】:
我会将此作为评论发布,但我没有足够的代表 - 对不起!。这对我来说真的很有用 - 感谢 rix0rrr,但我注意到 UTCToLocalTime 函数在最后构建日期时需要考虑区域设置。这是我在英国使用的版本 - 请注意 wDay 和 wMonth 的顺序是相反的:
Public Function UTCToLocalTime(dteTime As Date) As Date
Dim infile As FILETIME
Dim outfile As FILETIME
Dim insys As SYSTEMTIME
Dim outsys As SYSTEMTIME
insys.wYear = CInt(Year(dteTime))
insys.wMonth = CInt(Month(dteTime))
insys.wDay = CInt(Day(dteTime))
insys.wHour = CInt(Hour(dteTime))
insys.wMinute = CInt(Minute(dteTime))
insys.wSecond = CInt(Second(dteTime))
Call SystemTimeToFileTime(insys, infile)
Call FileTimeToLocalFileTime(infile, outfile)
Call FileTimeToSystemTime(outfile, outsys)
UTCToLocalTime = CDate(outsys.wDay & "/" & _
outsys.wMonth & "/" & _
outsys.wYear & " " & _
outsys.wHour & ":" & _
outsys.wMinute & ":" & _
outsys.wSecond)
End Function
【讨论】:
我想指出,作者询问了ISO8601格式的日期时间字符串,它们在字段顺序上是一致的。当然,您的数据适用于您的数据很棒,但如果其他人读到此内容并感到困惑,您应该查看en.wikipedia.org/wiki/ISO_8601 和xkcd.com/1179。 哇!过去的爆炸。无论如何,我没有更改 ISO 日期的字段顺序。它是 local 版本,需要遵循本地约定。理想情况下,代码应该可以解决这个问题,但我确实说过这是在英国使用的......【参考方案4】:我知道它不如 VB 模块那么优雅 但是,如果有人正在寻找一个考虑“+”之后的时区的快速公式,那么这可能就是它。
= DATEVALUE(MID(D3,1,10))+TIMEVALUE(MID(D3,12,5))+TIME(MID(D3,18,2),0,0)
会改变
2017-12-01T11:03+1100
到
2/12/2017 07:03:00 AM
(考虑时区的当地时间)
显然,你可以修改不同修剪部分的长度,如果你也有毫秒或者如果你在 + 之后有更长的时间。
如果您想忽略时区,请使用sigpwned
公式。
【讨论】:
秒转换对我来说看起来不对。 TIME() 接受参数小时、分钟、秒,2017-12-01T11:03+1100
中没有秒。如果有,例如2017-12-01T11:03:11+1100
那么我认为电话应该是TIME(0,0,MID(D3,18,2))
。但更简单地说,我正在使用这个:=DATEVALUE(MID(D3,1,10))+TIMEVALUE(MID(D3,12,8))
【参考方案5】:
您可以在不使用 VB 的情况下为应用程序执行此操作:
例如解析以下内容:
2011-01-01T12:00:00+05:00
2011-01-01T12:00:00-05:00
做:
=IF(MID(A1,20,1)="+",TIMEVALUE(MID(A1,21,5))+DATEVALUE(LEFT(A1,10))+TIMEVALUE(MID(A1,12,8)),-TIMEVALUE(MID(A1,21,5))+DATEVALUE(LEFT(A1,10))+TIMEVALUE(MID(A1,12,8)))
对于
2011-01-01T12:00:00Z
做:
=DATEVALUE(LEFT(A1,10))+TIMEVALUE(MID(A1,12,8))
对于
2011-01-01
做:
=DATEVALUE(LEFT(A1,10))
但 Excel 会自动解析上位日期格式。
然后您会得到一个 Excel 日期/时间值,您可以将其格式化为日期和时间。
详细信息和示例文件:http://blog.hani-ibrahim.de/iso-8601-parsing-in-excel-and-calc.html
【讨论】:
不幸的是,最后带有 Z 的解决方案的链接不再存在。 @hani - 您是否愿意直接插入解决方案,以便此答案保持其价值?【参考方案6】:我的日期格式为 20130221T133551Z (YYYYMMDD'T'HHMMSS'Z'),所以我创建了这个变体:
Public Function ISODATEZ(iso As String) As Date
Dim yearPart As Integer: yearPart = CInt(Mid(iso, 1, 4))
Dim monPart As Integer: monPart = CInt(Mid(iso, 5, 2))
Dim dayPart As Integer: dayPart = CInt(Mid(iso, 7, 2))
Dim hourPart As Integer: hourPart = CInt(Mid(iso, 10, 2))
Dim minPart As Integer: minPart = CInt(Mid(iso, 12, 2))
Dim secPart As Integer: secPart = CInt(Mid(iso, 14, 2))
Dim tz As String: tz = Mid(iso, 16)
Dim dt As Date: dt = DateSerial(yearPart, monPart, dayPart) + TimeSerial(hourPart, minPart, secPart)
' Add the timezone
If tz <> "" And Left(tz, 1) <> "Z" Then
Dim colonPos As Integer: colonPos = InStr(tz, ":")
If colonPos = 0 Then colonPos = Len(tz) + 1
Dim minutes As Integer: minutes = CInt(Mid(tz, 2, colonPos - 2)) * 60 + CInt(Mid(tz, colonPos + 1))
If Left(tz, 1) = "+" Then minutes = -minutes
dt = DateAdd("n", minutes, dt)
End If
' Return value is the ISO8601 date in the local time zone
' dt = UTCToLocalTime(dt)
ISODATEZ = dt
End Function
(时区转换未测试,没有意外输入时的错误处理)
【讨论】:
【参考方案7】:rix0rrr 的 answer 很棒,但它不支持没有冒号或只有小时的时区偏移。我稍微增强了功能以添加对这些格式的支持:
'---------------------------------------------------------------------
' Declarations must be at the top -- see below
'---------------------------------------------------------------------
Public Declare Function SystemTimeToFileTime Lib _
"kernel32" (lpSystemTime As SYSTEMTIME, _
lpFileTime As FILETIME) As Long
Public Declare Function FileTimeToLocalFileTime Lib _
"kernel32" (lpLocalFileTime As FILETIME, _
lpFileTime As FILETIME) As Long
Public Declare Function FileTimeToSystemTime Lib _
"kernel32" (lpFileTime As FILETIME, lpSystemTime _
As SYSTEMTIME) As Long
Public Type FILETIME
dwLowDateTime As Long
dwHighDateTime As Long
End Type
Public Type SYSTEMTIME
wYear As Integer
wMonth As Integer
wDayOfWeek As Integer
wDay As Integer
wHour As Integer
wMinute As Integer
wSecond As Integer
wMilliseconds As Integer
End Type
'---------------------------------------------------------------------
' Convert ISO8601 dateTimes to Excel Dates
'---------------------------------------------------------------------
Public Function ISODATE(iso As String)
' Find location of delimiters in input string
Dim tPos As Integer: tPos = InStr(iso, "T")
If tPos = 0 Then tPos = Len(iso) + 1
Dim zPos As Integer: zPos = InStr(iso, "Z")
If zPos = 0 Then zPos = InStr(iso, "+")
If zPos = 0 Then zPos = InStr(tPos, iso, "-")
If zPos = 0 Then zPos = Len(iso) + 1
If zPos = tPos Then zPos = tPos + 1
' Get the relevant parts out
Dim datePart As String: datePart = Mid(iso, 1, tPos - 1)
Dim timePart As String: timePart = Mid(iso, tPos + 1, zPos - tPos - 1)
Dim dotPos As Integer: dotPos = InStr(timePart, ".")
If dotPos = 0 Then dotPos = Len(timePart) + 1
timePart = Left(timePart, dotPos - 1)
' Have them parsed separately by Excel
Dim d As Date: d = DateValue(datePart)
Dim t As Date: If timePart <> "" Then t = TimeValue(timePart)
Dim dt As Date: dt = d + t
' Add the timezone
Dim tz As String: tz = Mid(iso, zPos)
If tz <> "" And Left(tz, 1) <> "Z" Then
Dim colonPos As Integer: colonPos = InStr(tz, ":")
Dim minutes As Integer
If colonPos = 0 Then
If (Len(tz) = 3) Then
minutes = CInt(Mid(tz, 2)) * 60
Else
minutes = CInt(Mid(tz, 2, 5)) * 60 + CInt(Mid(tz, 4))
End If
Else
minutes = CInt(Mid(tz, 2, colonPos - 2)) * 60 + CInt(Mid(tz, colonPos + 1))
End If
If Left(tz, 1) = "+" Then minutes = -minutes
dt = DateAdd("n", minutes, dt)
End If
' Return value is the ISO8601 date in the local time zone
dt = UTCToLocalTime(dt)
ISODATE = dt
End Function
'---------------------------------------------------------------------
' Got this function to convert local date to UTC date from
' http://excel.tips.net/Pages/T002185_Automatically_Converting_to_GMT.html
'---------------------------------------------------------------------
Public Function UTCToLocalTime(dteTime As Date) As Date
Dim infile As FILETIME
Dim outfile As FILETIME
Dim insys As SYSTEMTIME
Dim outsys As SYSTEMTIME
insys.wYear = CInt(Year(dteTime))
insys.wMonth = CInt(Month(dteTime))
insys.wDay = CInt(Day(dteTime))
insys.wHour = CInt(Hour(dteTime))
insys.wMinute = CInt(Minute(dteTime))
insys.wSecond = CInt(Second(dteTime))
Call SystemTimeToFileTime(insys, infile)
Call FileTimeToLocalFileTime(infile, outfile)
Call FileTimeToSystemTime(outfile, outsys)
UTCToLocalTime = CDate(outsys.wMonth & "/" & _
outsys.wDay & "/" & _
outsys.wYear & " " & _
outsys.wHour & ":" & _
outsys.wMinute & ":" & _
outsys.wSecond)
End Function
'---------------------------------------------------------------------
' Tests for the ISO Date functions
'---------------------------------------------------------------------
Public Sub ISODateTest()
' [[ Verify that all dateTime formats parse sucesfully ]]
Dim d1 As Date: d1 = ISODATE("2011-01-01")
Dim d2 As Date: d2 = ISODATE("2011-01-01T00:00:00")
Dim d3 As Date: d3 = ISODATE("2011-01-01T00:00:00Z")
Dim d4 As Date: d4 = ISODATE("2011-01-01T12:00:00Z")
Dim d5 As Date: d5 = ISODATE("2011-01-01T12:00:00+05:00")
Dim d6 As Date: d6 = ISODATE("2011-01-01T12:00:00-05:00")
Dim d7 As Date: d7 = ISODATE("2011-01-01T12:00:00.05381+05:00")
Dim d8 As Date: d8 = ISODATE("2011-01-01T12:00:00-0500")
Dim d9 As Date: d9 = ISODATE("2011-01-01T12:00:00-05")
AssertEqual "Date and midnight", d1, d2
AssertEqual "With and without Z", d2, d3
AssertEqual "With timezone", -5, DateDiff("h", d4, d5)
AssertEqual "Timezone Difference", 10, DateDiff("h", d5, d6)
AssertEqual "Ignore subsecond", d5, d7
AssertEqual "No colon in timezone offset", d5, d8
AssertEqual "No minutes in timezone offset", d5, d9
' [[ Independence of local DST ]]
' Verify that a date in winter and a date in summer parse to the same Hour value
Dim w As Date: w = ISODATE("2010-02-23T21:04:48+01:00")
Dim s As Date: s = ISODATE("2010-07-23T21:04:48+01:00")
AssertEqual "Winter/Summer hours", Hour(w), Hour(s)
MsgBox "All tests passed succesfully!"
End Sub
Sub AssertEqual(name, x, y)
If x <> y Then Err.Raise 1234, Description:="Failed: " & name & ": '" & x & "' <> '" & y & "'"
End Sub
【讨论】:
【参考方案8】:如果您只需将某些(固定)格式转换为 UTC 就足够了,您可以编写一个简单的 VBA 函数或公式。
下面的函数/公式适用于这些格式(无论如何都会省略毫秒):
2011-01-01T12:00:00.053+0500
2011-01-01T12:00:00.05381+0500
VBA 函数
更长,更好的可读性:
Public Function CDateUTC(dISO As String) As Date
Dim d, t, tz As String
Dim tzInt As Integer
Dim dLocal As Date
d = Left(dISO, 10)
t = Mid(dISO, 12, 8)
tz = Right(dISO, 5)
tzInt = - CInt(tz) \ 100
dLocal = CDate(d & " " & t)
CDateUTC = DateAdd("h", tzInt, dLocal)
End Function
...或“oneliner”:
Public Function CDateUTC(dISO As String) As Date
CDateUTC = DateAdd("h", -CInt(Right(dISO, 5)) \ 100, CDate(Left(dISO, 10) & " " & Mid(dISO, 12, 8)))
End Function
公式
=DATEVALUE(LEFT([@ISO], 10)) + TIMEVALUE(MID([@ISO], 12, 8)) - VALUE(RIGHT([@ISO], 5)/100)/24
[@ISO]
是包含 ISO8601 格式的本地时间日期/时间的单元格(在表格内)。
两者都会生成新的日期/时间类型值。随意根据您的需要调整功能(特定日期/时间格式)。
【讨论】:
以上是关于在 Excel 中解析 ISO8601 日期/时间(包括 TimeZone)的主要内容,如果未能解决你的问题,请参考以下文章
Tabulator 5.0 - 解析日期时间 luxon - 日期时间 ISO 8601
如何在 Swift 中解析/创建格式为小数秒 UTC 时区(ISO 8601、RFC 3339)的日期时间戳?
如何在 Swift 中解析/创建格式为小数秒 UTC 时区(ISO 8601、RFC 3339)的日期时间戳?