关于java:比较具有不同精度级别的Date对象

Compare Date objects with different levels of precision

我有一个JUnit测试失败,因为毫秒是不同的。 在这种情况下,我不关心毫秒。 如何更改断言的精度以忽略毫秒(或我想要设置的任何精度)?

我想传递的失败断言的示例:

1
2
3
4
5
Date dateOne = new Date();
dateOne.setTime(61202516585000L);
Date dateTwo = new Date();
dateTwo.setTime(61202516585123L);
assertEquals(dateOne, dateTwo);

还有另一种解决方法,我会这样做:

1
assertTrue("Dates aren't close enough to each other!", (date2.getTime() - date1.getTime()) < 1000);


有些库可以帮助解决这个问题:

Apache commons-lang

如果在类路径上有Apache commons-lang,则可以使用DateUtils.truncate将日期截断为某个字段。

1
2
assertEquals(DateUtils.truncate(date1,Calendar.SECOND),
             DateUtils.truncate(date2,Calendar.SECOND));

有一个简写:

1
assertTrue(DateUtils.truncatedEquals(date1,date2,Calendar.SECOND));

请注意,12:00:00.001和11:59:00.999会截断为不同的值,因此这可能不太理想。为此,有圆:

1
2
assertEquals(DateUtils.round(date1,Calendar.SECOND),
             DateUtils.round(date2,Calendar.SECOND));

AssertJ

从版本3.7.0开始,如果您使用的是Java 8 Date / Time API,AssertJ会添加一个isCloseTo断言。

1
2
3
4
LocalTime _07_10 = LocalTime.of(7, 10);
LocalTime _07_42 = LocalTime.of(7, 42);
assertThat(_07_10).isCloseTo(_07_42, within(1, ChronoUnit.HOURS));
assertThat(_07_10).isCloseTo(_07_42, within(32, ChronoUnit.MINUTES));

它也适用于遗留的Java日期:

1
2
3
Date d1 = new Date();
Date d2 = new Date();
assertThat(d1).isCloseTo(d2, within(100, ChronoUnit.MILLIS).getValue());


使用DateFormat对象,其格式仅显示要匹配的部分,并对生成的字符串执行assertEquals()。您也可以轻松地将它包装在您自己的assertDatesAlmostEqual()方法中。


你可以这样做:

1
assertTrue((date1.getTime()/1000) == (date2.getTime()/1000));

不需要字符串比较。


在JUnit中,您可以编写两个断言方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyTest {
  @Test
  public void test() {
    ...
    assertEqualDates(expectedDateObject, resultDate);

    // somewhat more confortable:
    assertEqualDates("01/01/2012", anotherResultDate);
  }

  private static final String DATE_PATTERN ="dd/MM/yyyy";

  private static void assertEqualDates(String expected, Date value) {
      DateFormat formatter = new SimpleDateFormat(DATE_PATTERN);
      String strValue = formatter.format(value);
      assertEquals(expected, strValue);
  }

  private static void assertEqualDates(Date expected, Date value) {
    DateFormat formatter = new SimpleDateFormat(DATE_PATTERN);
    String strExpected = formatter.format(expected);
    String strValue = formatter.format(value);
    assertEquals(strExpected, strValue);
  }
}

我不知道JUnit中是否有支持,但有一种方法可以做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.text.SimpleDateFormat;
import java.util.Date;

public class Example {

    private static SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy HH:mm:ss");

    private static boolean assertEqualDates(Date date1, Date date2) {
        String d1 = formatter.format(date1);            
        String d2 = formatter.format(date2);            
        return d1.equals(d2);
    }    

    public static void main(String[] args) {
        Date date1 = new Date();
        Date date2 = new Date();

        if (assertEqualDates(date1,date2)) { System.out.println("true!"); }
    }
}


这实际上是一个比它看起来更难的问题,因为边界情况下你不关心的方差超过你正在检查的值的阈值。例如毫秒差异小于一秒,但两个时间戳跨越第二个阈值,或分钟阈值或小时阈值。这使得任何DateFormat方法本身都容易出错。

相反,我建议比较实际的毫秒时间戳,并提供一个方差增量,表示您认为两个日期对象之间可接受的差异。一个过于冗长的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void assertDateSimilar(Date expected, Date actual, long allowableVariance)
{
    long variance = Math.abs(allowableVariance);

    long millis = expected.getTime();
    long lowerBound = millis - allowableVariance;
    long upperBound = millis + allowableVariance;

    DateFormat df = DateFormat.getDateTimeInstance();

    boolean within = lowerBound <= actual.getTime() && actual.getTime() <= upperBound;
    assertTrue(MessageFormat.format("Expected {0} with variance of {1} but received {2}", df.format(expected), allowableVariance, df.format(actual)), within);
}

为Joda-Time使用AssertJ断言(http://joel-costigliola.github.io/assertj/assertj-joda-time.html)

1
2
3
4
import static org.assertj.jodatime.api.Assertions.assertThat;
import org.joda.time.DateTime;

assertThat(new DateTime(dateOne.getTime())).isEqualToIgnoringMillis(new DateTime(dateTwo.getTime()));

测试失败消息更具可读性

1
2
3
4
5
6
java.lang.AssertionError:
Expecting:
  <2014-07-28T08:00:00.000+08:00>
to have same year, month, day, hour, minute and second as:
  <2014-07-28T08:10:00.000+08:00>
but had not.


使用JUnit 4,您还可以实现匹配器,以根据您选择的精度测试日期。在此示例中,匹配器将字符串格式表达式作为参数。对于此示例,代码不短。但是匹配器类可以重用;如果你给它一个描述名称,你可以用优雅的方式记录测试的意图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import static org.junit.Assert.assertThat;
// further imports from org.junit. and org.hamcrest.

@Test
public void testAddEventsToBaby() {
    Date referenceDate = new Date();
    // Do something..
    Date testDate = new Date();

    //assertThat(referenceDate, equalTo(testDate)); // Test on equal could fail; it is a race condition
    assertThat(referenceDate, sameCalendarDay(testDate,"yyyy MM dd"));
}

public static Matcher<Date> sameCalendarDay(final Object testValue, final String dateFormat){

    final SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);

    return new BaseMatcher<Date>() {

        protected Object theTestValue = testValue;


        public boolean matches(Object theExpected) {
            return formatter.format(theExpected).equals(formatter.format(theTestValue));
        }

        public void describeTo(Description description) {
            description.appendText(theTestValue.toString());
        }
    };
}

如果您使用Joda,您可以使用Fest Joda Time。


JUnit有一个内置的断言,用于比较双精度数,并指定它们需要的接近程度。在这种情况下,delta是您认为日期等效的毫秒数。此解决方案没有边界条件,测量绝对方差,可以轻松指定精度,并且不需要编写额外的库或代码。

1
2
3
4
5
6
7
8
    Date dateOne = new Date();
    dateOne.setTime(61202516585000L);
    Date dateTwo = new Date();
    dateTwo.setTime(61202516585123L);
    // this line passes correctly
    Assert.assertEquals(dateOne.getTime(), dateTwo.getTime(), 500.0);
    // this line fails correctly
    Assert.assertEquals(dateOne.getTime(), dateTwo.getTime(), 100.0);

注意它必须是100.0而不是100(或者需要加倍转换)以强制它将它们作为双精度进行比较。


只需比较您有兴趣比较的日期部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Date dateOne = new Date();
dateOne.setTime(61202516585000L);
Date dateTwo = new Date();
dateTwo.setTime(61202516585123L);

assertEquals(dateOne.getMonth(), dateTwo.getMonth());
assertEquals(dateOne.getDate(), dateTwo.getDate());
assertEquals(dateOne.getYear(), dateTwo.getYear());

// alternative to testing with deprecated methods in Date class
Calendar calOne = Calendar.getInstance();
Calendar calTwo = Calendar.getInstance();
calOne.setTime(dateOne);
calTwo.setTime(dateTwo);

assertEquals(calOne.get(Calendar.MONTH), calTwo.get(Calendar.MONTH));
assertEquals(calOne.get(Calendar.DATE), calTwo.get(Calendar.DATE));
assertEquals(calOne.get(Calendar.YEAR), calTwo.get(Calendar.YEAR));


这是一个实用功能,为我完成了这项工作。

1
2
3
    private boolean isEqual(Date d1, Date d2){
        return d1.toLocalDate().equals(d2.toLocalDate());
    }

像这样的东西可能会起作用:

1
2
assertEquals(new SimpleDateFormat("dd MMM yyyy").format(dateOne),
                   new SimpleDateFormat("dd MMM yyyy").format(dateTwo));

您可以创建一个小型协作者,而不是直接使用new Date,您可以在测试中模拟:

1
2
3
4
5
public class DateBuilder {
    public java.util.Date now() {
        return new java.util.Date();
    }
}

创建一个DateBuilder成员并将调用从new Date更改为dateBuilder.now()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.Date;

public class Demo {

    DateBuilder dateBuilder = new DateBuilder();

    public void run() throws InterruptedException {
        Date dateOne = dateBuilder.now();
        Thread.sleep(10);
        Date dateTwo = dateBuilder.now();
        System.out.println("Dates are the same:" + dateOne.equals(dateTwo));
    }

    public static void main(String[] args) throws InterruptedException {
        new Demo().run();
    }
}

主要方法将产生:

1
Dates are the same: false

在测试中,您可以注入DateBuilder的存根,并让它返回您喜欢的任何值。例如,使用Mockito或覆盖now()的匿名类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DemoTest {

    @org.junit.Test
    public void testMockito() throws Exception {
        DateBuilder stub = org.mockito.Mockito.mock(DateBuilder.class);
        org.mockito.Mockito.when(stub.now()).thenReturn(new java.util.Date(42));

        Demo demo = new Demo();
        demo.dateBuilder = stub;
        demo.run();
    }

    @org.junit.Test
    public void testAnonymousClass() throws Exception {
        Demo demo = new Demo();
        demo.dateBuilder = new DateBuilder() {
            @Override
            public Date now() {
                return new Date(42);
            }
        };
        demo.run();
    }
}

使用SimpleDateFromat将日期转换为String,在构造函数中指定所需的日期/时间字段并比较字符串值:

1
2
3
4
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String expectedDate = formatter.format(dateOne));
String dateToTest = formatter.format(dateTwo);
assertEquals(expectedDate, dateToTest);


我做了一个小课程,可能对一些最终在这里的谷歌有用:https://stackoverflow.com/a/37168645/5930242


您可以在比较日期时选择所需的精度等级,例如:

1
2
3
LocalDateTime now = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
// e.g. in MySQL db"timestamp" is without fractional seconds precision (just up to seconds precision)
assertEquals(myTimestamp, now);

我将对象转换为java.util.Date并进行比较

1
assertEquals((Date)timestamp1,(Date)timestamp2);