【问题标题】:Calculations in Excel spreadsheet using pre-1900 date使用 1900 年以前的日期在 Excel 电子表格中计算
【发布时间】:2021-07-09 14:40:30
【问题描述】:

Microsoft Excel 无法识别 1900 年前的日期,网上有大量信息记录了这一点,包括问题 here

最好的解决方法(许多其他帖子链接到)似乎是ExcelUser

但是,尽管解决方法可以让 Excel 将 1900 年之前的日期识别为日期,但它仍然不允许在计算中使用它,例如当想要计算自 1900 年前日期以来的年数时。

我的问题是是否可以修改 ExcelUser 中描述的解决方法以允许在计算中使用结果。

简单地说,例如,我想在 Excel 中计算自 1756 年 1 月 4 日以来的年数 - 这可能吗?

还是必须采用其他解决方案?也许有插件可以解决这个问题?

【问题讨论】:

  • 您的实际问题是什么? 究竟你想做什么?虽然 Excel 公式无法处理 1900 年以前的日期,但 VBA 可以。因此,如果您根据ISO 8601 使用proper 之类的yyyy-mm-dd 日期格式,则可以使用VBA 将该字符串拆分为使用DateSerial 的真实日期变量,并使用该格式进行计算以最终返回一个字符串。因此,您无法在公式中使用这些日期进行计算,但可以在 VBA 中进行计算。 • 如果您可以将问题范围缩小到您的实际问题,我可以尝试给出更好的答案。
  • 要添加到 PEH 的评论中,您可以使用DateDiff 获取任意两个日期之间的时间差。间隔可以是年、月、日等。
  • 我到底想做什么?如果我有一个日期电子表格,比如名人的生日,我想使用“Today()”公式计算他们的年龄。这样我就可以留意任何重要的周年纪念日,例如X 先生出生 200 年或 Y 夫人出生 500 年。

标签: excel vba date


【解决方案1】:

有一个比接受的答案更简单的方法。只需将您的日期转换为 Unix 时间:

Function nUnixTime(dTimestamp As Date) As LongLong
' Return given VB date converted to a Unix timestamp.

Const nSecondsPerDay As Long = 86400        ' 24 * 60 * 60

nUnixTime = Int(CDbl(CDate(dTimestamp) - CDate("1/1/1970"))) * nSecondsPerDay

End Function

Unix time 是自 1970 年 1 月 1 日以来的秒数,该日期之前的时间为负数。因此,如果您将日期转换为 Unix 时间,您只需将它们相减,然后将结果除以 86,400 即可得出天数之差,或者除以 31,557,600 年份(31,557,600 = 60 * 60 * 24 * 365.25)。

上述从 Excel 调用的 VB 函数的示例结果:

Column A Column B formula Column B value
2/2/2022 =nUnixTime(A1) 1,643,760,000
1/1/1970 =nUnixTime(A2) 0
12/14/1901 =nUnixTime(A3) -2,147,472,000
12/13/1901 =nUnixTime(A4) -2,147,558,400
1/1/1900 =nUnixTime(A5) -2,208,988,800
1/1/1800 =nUnixTime(A6) -5,364,662,400
1/1/100 =nUnixTime(A7) -59,011,459,200

我在 1901 年包含这两个日期的原因是因为它们在 Unix 时间中的大小刚好小于或大于有符号 32 位整数的最大值,即 VBA 中的 Long。如果上述函数的输出是 Long,那么 1901 年 12 月 14 日之前的日期值将是错误 #Value!。这就是函数的输出被定义为LongLong的原因,它是VBA的有符号64位整数。

【讨论】:

  • 请注意,这有一个大问题。 1/1/1800 必须是 String,因为 Excel 不支持 1900 年之前的日期。因此,如果将此字符串推入 dTimestamp As Date,Excel 将无法知道该字符串是 m/d/yyyy 还是 d/m/yyy,因此 Excel 必须“猜测”这使用您当前的 Windows 日期格式设置。因此,这可能在一台计算机上有效,而在另一台计算机上失败。唯一知道 A 列中哪种格式的人是将字符串放在那里的人,Excel 无法知道。因此,要让它可靠地工作,就无法拆分字符串。不要让 Excel 猜测。
【解决方案2】:

如果您的日期是明确的格式(例如 ISO 或与您的 Windows 区域设置相对应的格式,或者如果在 1900 年之后是真实日期),您可以使用 VBA 来识别早期日期。

Function Age(dt As Date)
    Age = DateDiff("yyyy", dt, Date)
End Function

您应该知道,由于函数计算年份差异的方式,取决于您想要的结果,如果生日在今天之前/之后,您可能需要调整答案。

换句话说,如果生日的年份在今天之后,您可能需要从结果中减去1

但这应该让你开始。

【讨论】:

  • 谢谢,这很好而且很简单,但是手动减去 1 的想法是一个关闭。当然,这个问题并没有准确定义年龄的含义,但通常我们不会说某人是例如“45”,直到他们实际上已经过了给定年份的日和月周年纪念日。出于这个原因,@Pᴇʜ 的 OldDateAge 对我的目的很有用。
【解决方案3】:

首先,我强烈建议对日期使用ISO 8601 格式yyyy-mm-dd,因为即使您只有字符串而没有真正的数字日期,这也是可正确排序的,并且是唯一明确定义且不会被误解的日期格式比如02/03/2021,没有人能说它是mm/dd/yyyy还是dd/mm/yyyy,因为两者都确实存在。

由于旧日期不能是真正的数字日期,而只能作为字符串输入(看起来像日期),这意味着需要避免误解,否则您会得到错误的结果。因此,不会被误解的日期格式是一个明显的优势。

其次,计算“X先生出生多少年”的方法不止一种:例如,将Maryam Mirzakhani1977-05-12的生日与今天的日期进行比较2021-04-15。今天她今年还没有过生日,因此她将是43 岁。但今年她会变成44 年(2021 - 1977 = 44)。所以这个问题需要问得更准确。 “X 先生今天几岁?” 或“X 先生今年几岁”。计算方式会有所不同。

让我们开始并假设以下数据。我们已经知道 Excel 无法使用 1900 年之前的日期进行计算。您可以看到,如果我们输入 1900 年之前的日期,它们会被格式化为字符串(红色日期),而 1900 年之后的日期会被格式化为数字日期(绿色日期)。

图 1:#WERT! 表示 #VALUE!(对于德语截图很抱歉)。

同样在 D 列中,使用了公式 =DATEDIF($B2,TODAY(),"y") 的字符串日期无法计算。但由于 VBA 实际上可以处理 1900 年前的日期,我们可以为此编写自己的 UDF(用户定义函数)。因为正如我上面解释的,有 2 种不同的计算方法,所以有 2 种不同的函数:

  • OldDateDiff(Date1, Date2, Interval) 称为 =OldDateDiff($B2,TODAY(),"yyyy")
  • OldDateAge(Date1, Date2) 称为 =OldDateAge($B2,TODAY())
Option Explicit

Public Function OldDateDiff(ByVal Date1 As Variant, ByVal Date2 As Variant, ByVal Interval As String) As Long
    Dim RetVal As Long 'variable for the value we want to return
    
    Dim localDate1 As Date
    If VarType(Date1) = vbDate Or VarType(Date1) = vbDouble Then 'check if Date1 is numeric
        localDate1 = CDate(Date1) 'if numeric take it
    ElseIf VarType(Date1) = vbString Then 'check if Date1 is a string
        localDate1 = ISO8601StringToDate(Date1) 'if it is a string convert it to numeric
    Else 'neither string nor numeric throw an error
        RetVal = CVErr(xlErrValue)
        Exit Function
    End If
    
    Dim localDate2 As Date 'same as for Date1 but with Date2
    If VarType(Date2) = vbDate Or VarType(Date2) = vbDouble Then
        localDate2 = CDate(Date2)
    ElseIf VarType(Date2) = vbString Then
        localDate2 = ISO8601StringToDate(Date2)
    Else
        RetVal = CVErr(xlErrValue)
        Exit Function
    End If
    
    If localDate1 <> 0 And localDate2 <> 0 Then 'make sure both dates were filled with values
        RetVal = DateDiff(Interval, localDate1, localDate2) 'calculate the difference between dates with the desired interaval eg yyyy for years
    End If
    
    OldDateDiff = RetVal 'return the difference as result of the function
End Function

Public Function OldDateAge(ByVal Date1 As Variant, ByVal Date2 As Variant) As Long
    Dim RetVal As Long 'variable for the value we want to return
    
    Dim localDate1 As Date
    If VarType(Date1) = vbDate Or VarType(Date1) = vbDouble Then 'check if Date1 is numeric
        localDate1 = CDate(Date1) 'if numeric take it
    ElseIf VarType(Date1) = vbString Then 'check if Date1 is a string
        localDate1 = ISO8601StringToDate(Date1) 'if it is a string convert it to numeric
    Else 'neither string nor numeric throw an error
        RetVal = CVErr(xlErrValue)
        Exit Function
    End If
    
    Dim localDate2 As Date 'same as for Date1 but with Date2
    If VarType(Date2) = vbDate Or VarType(Date2) = vbDouble Then
        localDate2 = CDate(Date2)
    ElseIf VarType(Date2) = vbString Then
        localDate2 = ISO8601StringToDate(Date2)
    Else
        RetVal = CVErr(xlErrValue)
        Exit Function
    End If
    
    If localDate1 <> 0 And localDate2 <> 0 Then 'make sure both dates were filled with values
        RetVal = WorksheetFunction.RoundDown((localDate2 - localDate1) / 365, 0)
          'subtract date1 from date2 and divide by 365 to get years, then round down to full years to respect the birthday date.
    End If
    
    OldDateAge = RetVal 'return the age as result of the function
End Function


' convert yyyy-mm-dd string into numeric date
Private Function ISO8601StringToDate(ByVal ISO8601String As String) As Date
    Dim ISO8601Split() As String
    ISO8601Split = Split(ISO8601String, "-") 'split input yyyy-mm-dd by dashes into an array with 3 parts
    
    ISO8601StringToDate = DateSerial(ISO8601Split(0), ISO8601Split(1), ISO8601Split(2)) 'DateSerial returns a real numeric date
                                   '     ≙yyyy             ≙mm              ≙dd
End Function

请注意,此处 B 列包含 2 种不同类型的数据。字符串(看起来像日期)和实数日期。如果您对它们进行排序,所有数字日期将在字符串日期之前排序(这可能不是您想要的)。因此,如果您希望它可以按生日列排序,请确保将 all 日期转换为字符串。这可以通过在每个日期前面添加撇号 ' 来完成。这不会显示,但请确保输入的日期被视为字符串。

【讨论】:

  • Vielen Dank mein guter Freund,@Pᴇʜ! 非常感谢你能详细介绍!我在网上查找了如何在 Excel 中插入用户定义的函数——(this tutorial 很有帮助)。我没有正确理解您的 ISO8601StringToDate 函数的目的——它似乎通过在前面插入一个撇号 ' 来强制以 yyyy-mm-dd 格式写入的 1900 年前的日期到字符串?我必须插入调用这个函数还是在后台工作?
  • @AdAstm 不客气。实际上ISO8601StringToDate 只是将yyyy-mm-dd 格式的字符串转换为VBA 中的数字日期。这是必要的,因为您无法使用字符串进行计算,您需要一个数字日期来减去它们或计算差异。它被其他两个程序自动调用,例如。在localDate1 = ISO8601StringToDate(Date1) 行中,它将Date1 中的字符串转换为实数日期并写入localDate1。我在代码中添加了一些cmets,希望对你有帮助,如果没有你可以问。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-14
  • 2011-01-27
  • 1970-01-01
  • 1970-01-01
  • 2014-05-19
  • 2023-01-13
相关资源
最近更新 更多