最近博主遇到一个日期转换不符合预期的问题。
具体现象就是一个“17JUN38”的生日想转换为“yyyy-MM-dd”格式的日期转成了“2038-06-17”。生日比当前时间还大,明显出错了。
//当时出错的方法
private String dateSwitch(String date){ SimpleDateFormat inSdf = new SimpleDateFormat("ddMMMyy",Locale.ENGLISH); SimpleDateFormat outSdf = new SimpleDateFormat("yyyy-MM-dd"); Date paramDate = null; try { paramDate = inSdf.parse(date); } catch (ParseException e) { LOGGER.warn("",e); } return outSdf.format(paramDate); }
明明原始时间只告诉了两位年份,能给你转出四位年份都不错了!还要什么自行车!摔!
博主很生气,想跟产品理论,但是想想产品经理那40米的长刀,哎,算了我还是自己想想办法吧。
到这里肯定有小伙伴肯定想到了,既然是生日,那肯定比当前时间小,先转一下,如果时间比当前大,就减去100年呗。
这么简单的解决办法博主作为一个资深的开发能想不到么?(手动狗头)
开个玩笑,博主还是个菜鸟,主要是为什么要减去100年呢?写代码这种事情很严肃的,不能说你看到一个现象转换出错的“2038-06-17”比预期的“1938-06-17”差一百年你就得出结论。不明白的代码不能乱用,不然到时候坑人坑己(严肃脸)。所以博主去偷偷研究了一下parse的源代码。
在SimpleDateFormat的构造函数最后面,会调用initialize()方法,然后initialize()方法会调用下面这么一段代码:
/* Initialize the fields we use to disambiguate ambiguous years. Separate * so we can call it from readObject(). */ private void initializeDefaultCentury() { calendar.setTimeInMillis(System.currentTimeMillis()); calendar.add( Calendar.YEAR, -80 ); parseAmbiguousDatesAsAfter(calendar.getTime()); } /* Define one-century window into which to disambiguate dates using * two-digit years. */ private void parseAmbiguousDatesAsAfter(Date startDate) { defaultCenturyStart = startDate; calendar.setTime(startDate); defaultCenturyStartYear = calendar.get(Calendar.YEAR); }
这里会初始化一个叫做defaultCenturyStart以及defaultCenturyStartYear的属性,请大家记住这俩字段。
public Date parse(String text, ParsePosition pos) { …… …… …… //其他代码就略去了,有兴趣的同学可以自己看看源码// At this point the fields of Calendar have been set. Calendar // will fill in default values for missing fields when the time // is computed. pos.index = start; Date parsedDate; try { parsedDate = calb.establish(calendar).getTime(); // If the year value is ambiguous, // then the two-digit year == the default start year if (ambiguousYear[0]) { if (parsedDate.before(defaultCenturyStart)) {//------① parsedDate = calb.addYear(100).establish(calendar).getTime(); } } } // An IllegalArgumentException will be thrown by Calendar.getTime() // if any fields are out of range, e.g., MONTH == 17. catch (IllegalArgumentException e) { pos.errorIndex = start; pos.index = oldStart; return null; } return parsedDate; }
这里只粘了部分跟这个问题有关的代码,因为篇幅太长就不一一往这里贴了,有兴趣的小伙伴可以自己去看看全部的源码哦!
这里可以看到:
If the year value is ambiguous,then the two-digit year == the default start year
要是传入的年份描述不清楚,那就使用默认的起始年去补全
那么用来补全不完整的年份的默认起始年是个什么东东呢?这时候前面让大家记住的defaultCenturyStartYear字段就有用了。这个字段里面存的是当前时间的年份减去80年的年份,比如说我当前2018-09-13,减80年是1938年,然后程序会用这个19去补全传入的年份,用前面的例子,就是补成了1938-06-17。
紧接着下面有一个判断语句①。defaultCenturyStart字段也是就是当前时间减去80年,就是1938-09-13,1938-06-17在1938-09-13之前,于是程序便进入判断体,将年份加上100,得到了2038-06-17。
好了到了这里就破案了,所以之前的猜测有了证据支撑,变成了结论。
这个时候,如果有跟我一样好奇的小伙伴可能会问了,为什么要用这个80作为标准呢?
其实博主猜测是编写这个大佬经过科学计算或者什么调查统计得到的一个数字吧,当然没有博主没有对此做过专门的调查,并没有发言权。我们来看看这个标准能不能修改:
/** * Sets the 100-year period 2-digit years will be interpreted as being in * to begin on the date the user specifies. * * @param startDate During parsing, two digit years will be placed in the range *startDate
tostartDate + 100 years
. * @see #get2DigitYearStart * @since 1.2 */ public void set2DigitYearStart(Date startDate) { parseAmbiguousDatesAsAfter(new Date(startDate.getTime())); }
这里提供了一个public的方法,可自己手动设置前面黑体的两个字段,可以根据自己的需求来做出改变。
所以最终代码改为
private String dateSwitch(String date){ SimpleDateFormat inSdf = new SimpleDateFormat("ddMMMyy",Locale.ENGLISH); SimpleDateFormat outSdf = new SimpleDateFormat("yyyy-MM-dd"); Date paramDate = null; try { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.YEAR, -100); inSdf.set2DigitYearStart(calendar.getTime()); paramDate = inSdf.parse(date); } catch (ParseException e) { LOGGER.warn("",e); } return outSdf.format(paramDate); }
好了总结一下:
当传入一个年份不完整的日期两位(或者三位,一位不行),程序会用当前时间减去80年的前两位去补全日期,得到时间A,然后跟当前时间减去80年,得到时间B。如果A在B之前,那么便将时间A做+100年运算,得到最终时间C。80年这个标准可以通过set2DigitYearStart()方法来自定义,来满足不同的需求。