SimpleDateFormat 一小时之谜

背景介绍


近期,编写了一个针对JavaScript日期格式化类的测试。这个JavaScript日期格式化类模仿Java中的SimpleDateFormat类(java.text.SimpleDateFormat)的格式。


测试采用QUnit作为JavaScript单元测试工具。结合QUnit,同时需要SimpleDateFormat类的格式输出结果做比对,那么最为简便的测试环境是使用JSP(容器/服务器可选择Tomcat),测试的步骤如下:



  1. 定义格式化模式(Pattern),这个模式同时需要在 JSP页面 和 测试脚本中定义。比如:”HH:mm:ss”;

  2. 在JSP页面中,使用SimpleDateFormat类格式化日期,并将日期原始数据和格式化结果写在页面上。例如:
    <div class=”date”><%= date.getTime()%></div>
    <div class=”result”><%= formatter.format(date)%></div>

  3. 在测试脚本中,获取页面上的日期原始数据并进行格式化;将结果与(2)中输出的结果进行比对。


测试分为“模式(Pattern)”测试,“本地化(Locale)”测试,“边界值(Boundary)”测试以及“随机(Random)”测试。其中:






  • 模式测试,是指针对单个模式(如”y”, “yy”, “yyyy”)进行测试。模式字母个数的不同,输出的结果也不同,如假定年份是2013,那么模式”y”和”yy”输出是13,而”yyyy”输出则是2013。

  • 本地化测试,是指针对时区进行测试,在国际化的场景中比较实用。比如测试(GMT 0)以及本地时区(GMT +8)的数据相差值是否正确。

  • 边界值测试,是指针对边界情况进行测试。比如,当选择2月份,并错误的将天数设定为30天,此时应该可以向前推算,变成3月1号/3月2号。

  • 随机测试,是指随机生成样例进行大范围的测试。



“一小时”之谜


假定 JavaScript 日期格式化类定义在Example.Date,格式方法为format


在进行随机测试的过程(随机生成100样例)中,发现会有一定的个数出现失败。



// 随机测试的JSP代码
SimpleDateFormat sdfRandom = new SimpleDateFormat(“yyyy-MM-dd HH:mm E a h”);
Random rand = new Random();
for (int i = 0; i < 100; i++) {
int year = 1970 + rand.nextInt(2014 - 1970);
int month = rand.nextInt(12);
int day = rand.nextInt(32);
int hour = rand.nextInt(24);
int minute = rand.nextInt(60);
int second = rand.nextInt(60);
cal.set(year, month, day, hour,minute, second);
date = cal.getTime();
// 输出结果到页面
}

 // 测试脚本代码
test(“random”, function() {
var date = null;
var result = null;
var jsFormatter = Example.Date.format;
var jsResult = null;
var lengthOfTestcase = document.querySelectorAll(“.random-test-case”).length;
for (var i = 0; i < lengthOfTestcase; i++) {
date = document.getElementById(“random-“ + i + “-date”).innerHTML;
result = document.getElementById(“random-“ + i + “-result”).innerHTML;
jsResult = jsFormatter(“yyyy-MM-dd HH:mm E a h”, date);
equal(jsResult, result, “The format should be:” + result);
}
});

所有失败样例的特点都一致,即SimpleDateFormat输出的结果比Example.Date.format输出的结果在“小时”上刚刚好多一个小时:


Expected: “1986-09-03 06:06 Wed AM 6”
Result: “1986-09-03 05:06 Wed AM 5”
Diff: “1986-09-03 06:06 05:06 Wed AM 6” 5”


定位问题

经过一番排查之后,最终发现最大的可疑点。例如:Unix时间戳526079183在SimpleDateFormat输出与在JavaScript中输出结果不一致:


// JavaScript
console.log(new Date(526079183*1000));
// Wed Sep 03 1986 05:06:23 GMT+0800 (China Standard Time)


// Java SimpleDateFormat
formatter.format(526079183000L);
// 1986-09-03 06:06:23

用其他语言进行佐证,比如采用Ruby:


# Ruby irb
Time.at(526079183)
# => 1986-09-03 05:06:23 +0800

可以看到JavaScript的输出与Ruby的输出一致,那么原因在于SimpleDateFormat上。


问题的可能原因在于SimpleDateFormat中会涉及到夏令时(Daylight saving time)。Wikipedia上关于夏令时的定义:


夏时制,又称日光节约时制、日光節約時間(英语:Daylight saving time)或夏令时间(英语:Summer time),是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”。一般在天亮早的夏季人为将时间提前一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。各个采纳夏时制的國家具体规定不同。目前全世界有近110个国家每年要实行夏令时。

其中,中国在1986年至1991年之间实行过夏令时。

验证猜想

可以从两点上进行判断是否是由于夏令时导致:



  1. 设置SimpleDateFormat的时区为GMT 0以避免夏令时。
    SimpleDateFormat sdfRandom = new SimpleDateFormat(“yyyy-MM-dd HH:mm E a h”);
    sdfRandom.setTimeZone(TimeZone.getTimeZone(“UTC”));
    Random rand = new Random();

  2. 将随机的样例数据范围设定在实行夏令时的时间段内,那么失败样例的比例应该会大幅提高。

    int year = 1986 + rand.nextInt(1991 - 1986);



结果数据上验证了猜想。在采用夏令时的时间上,SimpleDateFormate输出的结果将多出一个小时,即提前了一个小时。

0%