经过多天的折腾,终于将Liferay Calendar Portlet打造成一个完美支持多种历法(目前界面上支持GregorianCalendar和ChineseCalendar)的一个组件了。
折 腾的初衷是因为目前所有的日历组件中,很多都有定期提示功能,但是都只是在GregorianCalendar,也就是我们用的公历上,比如每年几月几 日。 而我们国内还有很多事情是用农历的,比如好友的生日,很多人的生日都是只过农历生日。这样就给惦记他/她的人造成很大的麻烦,经常记不住今年生日到底是哪 天(目前只见过一个叫掌上万年历的软件支持农历生日提醒)。还有其他周期的农历重复事件就更难以实现了,比如,每个农历的初一的提醒等等。
在当前这个网站的Calendar组件中也支持农历了,但是支持的很不好,只能实现每年定期,而不能实现其他的重复条件。
偶 尔间发现了ibm的一个开源组件,ICU (International Components for Unicode),这是一个实现国际化的基础工具包,其中就包含关于各种历法的支持。而且更爽的是其API设计是完全按照JDK Calendar的命名来的,也就是说JDK Calendar有的api, ICU都有,类名也相同。通过继承实现了不同历法的支持。
(这 里不得不感慨一下,这种国际大公司的竞争力确实不是我们一些本土企业所能比拟的,他们在很多领域遇到的问题是我们没有遇到过的,而且他们多积累的这些基础 代码,以及里面所相关的知识更是很难在短时间内能获得的。虽然我们可以通过各种开源工具来弥补与这些巨头之间的差距,但是,对于这些开源工具的掌握本身就 不是一个容易的事情。另外,外国公司对于我们中国的农历的知识积累丝毫不逊于我们国内一些研究机构,看看里面一些关于ChineseCalendar相关 的论文就知道了,不得不让我们再次汗颜啊)
我的工作正是基于IBM ICU来实现的。虽说已经是站在巨人的肩膀上了,但是很多东西还是没有那么顺利。主要就是历法转换,比较,时区等等问题。
如果说要列选一下编程中比较难搞的算法,与时间相关的肯定不在少数(是不是因为人们只生活在一个三维空间,对于四维的思考能力就很差?)。尤其是涉及到时间转换,比较的,如果再加上时区转换就更加要命了。
Liferay Calendar的核心算法在Recurrence中,里面有一个isInRecurrence,用来判断一个日期是否是在重复周期上的算法逻辑。该算法 能满足iCalendar标准的所有重复方式,按天,月,年,不同frequency, interval, duration, until等等,其算法描述如下:
1. 先统一为某个TimeZone; 2. 如果current为历史时间,返回false 3. 如果Current正好在startDate到startDate + Duration之间,返回真(还在持续期); 否则: [按最小步频向startDate靠拢,如果最终日期落在startDate ~ Duration之间(或者符合按频率的日期,比如每周五,但startDate不是周五), 返回真,否则返回假;] 4. 根据current计算下一个可能的日期candidate 4.1. 获取一个最小interval (递减的步频) 4.2. 将秒/分/时拨回到与startDate一样; 4.3. 按照minInterval 向回滚动,返回滚动后日期 5. 如果candidate是重复日期,返回真, 否则: 5.1. candidate超过(晚于)until, 返回假; 5.2. interval间隔不匹配,或者重复次数已经超出,返回假; 5.3. 如果不是任何一个重复规则,返回假; 5.4. 否则返回真 6. 将candidate向回一秒,值作为新的candidate,如果candidate早于startDate,则返回假; 7. 重复5 |
这个只是算法的框架,还有很多细节就只能参考源码来了解了。
原有的代码只是支持两个日期都是GregorianCalendar, 并不支持其他的历法。采用ICU之后,就可以比较方便地实现其他历法了,只要将两个要比较的日期转换为相同的历法。
关于日期的比较,需要满足两个前提:1)相同历法, 2)相同TimeZone. 在北京的时间与纽约时间比较是没有意义的。
而TimeZone的问题,就比较复杂了,需要看看Calendar源码才能搞清楚。下面把我总结的一些经验罗列一下:
1)Calendar 对象只要创建之后,其timeInMillies属性就是确定的,这个属性是一个long值,记录了到1970.1.1(好像是)的毫秒差。该属性值与时 区无关。这里用了类似信息与展现分离的设计思想,信息就是不变的timeInMillies,展现就是各种不同的历法,不同的时区。
2)不同时区通过ZONE_OFFSET值与timeInMillies值相加,可以计算calendar对象在当前时区的相对时间,比如年,月,日,小时、分、秒。
3)Calendar的set(field, value)方法只进行对应field的设置,不计算由于这个field变化引起其他field的变化,只有在下一次get操作时,才会进行计算。
4) 有两种需求会用到setTimeZone(zone):时区转换和日历比较,时区转换只需要设置为目标时区的TimeZone就好了,而日历比较则还需要 将ZONE_OFFSET, DST_OFFSET等field清空。否则计算其他field时,这两个fields会参与运算,得出你不想要的值出来。
这个工作是网站改版的一部分,不久大家就能在我的网站 上用上这个功能了,呵呵,敬请期待。