时间点

表示某个时间点,具有某种粒度

2004 年 3 月 7 日

这是我在 2000 年代中期进行的 企业应用程序架构进一步开发 写作的一部分。不幸的是,此后太多其他事情吸引了我的注意力,所以我没有时间进一步研究它们,而且在可预见的未来我也看不到太多时间。因此,这些材料非常草稿形式,我不会进行任何更正或更新,直到我有时间再次处理它。

时间点,很明显,是时间中的一个点。我正在写这篇文章,时间是 2000 年 8 月 28 日。这是一个时间点。这就引出了一个问题,为什么要将时间点作为一种模式来写?毕竟它们现在是许多语言和大多数类库的一部分。

问题在于,时间点有一些微妙之处,即使是构建库的人也可能没有意识到。

工作原理

时间点最常见的问题是,它们以不同的精度出现。当我说我正在写这篇文章,时间是 2000 年 8 月 28 日,以及我说我正在写这篇文章,时间是 2000 年 8 月 28 日下午 2:33:34 时,我说的是两件不同的事情。一个陈述是精确到天,另一个是精确到秒。请注意,秒精度比天精度更精确,但在这种情况下,恰好不太准确。任何时间点都需要知道它的精度,以便您可以回答诸如此事件是否与另一个事件同时发生之类的疑问。

关键点是,您不能仅依靠大多数领域的精度。许多业务都是以天为精度进行的。我的手机转账请求在一天中的什么时候发生并不重要,它只是根据我进行转账的那一天进行处理。否则生活会变得非常烦人。如果我想在账单提交付款的同一天进行转账,我是否应该担心账单提交的确切时间。常见的商业惯例说,不,如果是在同一天,我就不会冒透支或拒绝付款的风险。

注意不要使用比您需要更精确的时间点。许多平台只提供精确到秒或更高精度的時間點。那么我如何表示 2000 年 8 月 28 日的任何时间?通常,您使用一种约定,例如在午夜“00:00:00”整。这在某些情况下可能有效,但问题会逐渐显现。将几毫秒泄漏到时间点中往往出奇地容易,这时您会遇到问题,因为 2000 年 8 月 28 日不等于 2000 年 8 月 28 日。

时间点的另一个棘手领域是如何处理时区。与精度类似,没有适用于所有应用程序的正确答案。有时您需要带有时区的时间点,有时不需要。没有时区的时间点是完全合理的,这意味着它在它所在上下文的本地时间内。您会在以下情况下找到它们:进一步的时区信息要么没有用,要么可以从它的上下文中获取。如果您不需要时区,请谨慎使用它。

我遇到的一个让我非常困扰的这个问题出现在微软的 Outlook 中。您输入的任何约会时间(至少在 Outlook 98 中)都是特定于时区的。因此,如果我移动时区并更改了笔记本电脑上的时间,会议时间也会改变(除非我在最初将时间输入 Outlook 时考虑到了这一点——大多数人不会这样做)。这加剧了这样一个事实:如果我选择了一整天会议,它不会将这一天作为一天精度的時間點,而是选择了一个 时间点范围。因此,我在波士顿输入的一整天会议,当我飞往芝加哥时,变成了从晚上 11 点到晚上 11 点的会议。更糟糕的是,我的 WinCE 机器只以天为精度显示一整天会议,使用范围的开始,从而将我的一整天约会移动到前一天。

在我的 时间点 讨论中,时间点的一个重要特征是它们是锚定的,也就是说,时区指的是时间线上特定的一点。值为下午 2:30 的时间对象没有锚定,因为它可能意味着任何一天的下午 2:30。如果有一个天精度的时间点来为下午 2:30 提供上下文,那么下午 2:30 与天精度时间一起表示一个(正确锚定的)时间点。

时间点类的一个显而易见且常见的服务是获取当前时间点。通常,这是通过使用操作系统查询系统时钟来完成的。但是,在这里添加间接性是一个好主意。测试通常需要稳定的时间,而且通常即使是操作也可能需要在星期一运行系统,就像上周五一样。因此,除了访问系统日期之外,还值得访问处理日期,它可能与系统日期相同,也可能不同。此外,处理日期应该在操作期间可设置。当您使用日志记录时,您可能需要在日志中使用处理日期和系统日期。

何时使用它

某种形式的时间点几乎无处不在。使用它们的关键问题是决定上面讨论的精度和时区问题。只有在您的域需要时才使用天精度的时间点,不要尝试使用约定或 范围 来伪造它,即使这意味着要编写自己的包装类。同样,除非您确实需要,否则不要使用时区。考虑一下使用系统的人如何看待世界,许多人除非需要,否则不会考虑时区。

示例:一个简单的日期精度包装器(Java)

这是我在这里用于示例的简单日期精度包装器的一部分。基本结构包装了一个 Java 格里高利日历实例

class MfDate...

  private GregorianCalendar _base;
public MfDate() {
  this(new GregorianCalendar());
}
public MfDate(int year, int month, int day) {
  initialize (new GregorianCalendar(year, month - 1, day));
}
  private void initialize (GregorianCalendar arg) {
      _base = trimToDays(arg);
  }
  private GregorianCalendar trimToDays(GregorianCalendar arg) {
      GregorianCalendar result = arg;
      result.set(Calendar.HOUR_OF_DAY,0);
      result.set(Calendar.MINUTE, 0);
      result.set(Calendar.SECOND, 0);
      result.set(Calendar.MILLISECOND, 0);
      return result;
  }

请注意,我通过将格里高利日历中的适当值设置为零来强制进行修剪。我还提供了一个使用美国风格数字参数的构造函数。由于我在书籍代码中经常使用这些,因此我最好让它变得容易……

以下是比较操作,这是一个我仅委托给底层对象的示例行为。

class MfDate...

  public boolean after (MfDate arg) {
    return getTime().after(arg.getTime());
  }
  public boolean before (MfDate arg) {
    return getTime().before(arg.getTime());
  }
  public int compareTo(Object arg) {
    MfDate other = (MfDate) arg;
    return getTime().compareTo(other.getTime());
  }
  public boolean equals(Object arg) {
    if (! (arg instanceof MfDate)) return false;
    MfDate other = (MfDate) arg;
    return (_base.equals(other._base));
  }
    public Date getTime() {
        return _base.getTime();
    }

包装器还允许我添加我认为方便但基本类中没有的行为。

class MfDate...

  public MfDate addDays(int arg) {
    return new MfDate(new GregorianCalendar(getYear(), getMonth(), getDayOfMonth() + arg));
  }
    public MfDate minusDays(int arg) {
        return addDays(-arg);
    }