Javascript日期的Format与Parse
网上已经有很多文章或代码介绍了如何用javascript格式化一个Date对象,但都和自己的应用与要求有一定的差距。尤其是如何Parse一个字符串的日期,大多依赖Date对象的parse方法,在有些应用中对如“2012年3月8日”这样的格式就需要进行特殊处理,甚至于据说IE浏览器对“2012-3-8”的格式似乎都不能直接parse(我没有太多这方面的经验)。
我一直想仿照Java的SimpleDateFormat做一个javascript版的Formatter和Parser以完善 项目。最近学习了javascript的正则表达式,并在用javascript进行HTML转义与反转的实现中对正则表达式的应用有了进一步的熟悉。所以决定用正则表达式来比较彻底地实现一个javascript版的SimpleDateFormat。
估计发明轮子的感觉是我这个年龄的程序员,如果还在(喜欢)编程的唯一的动力了。
** 一、SimpleDateFormat的功能
参照Java的SimpleDateFormat,这个javascritp版的主要应有以下功能:
*** 1)符合Java的模式符号
Java在格式化日期提供了很多模式符号,有些是我们几乎从未用过的,在javascript的版本里,主要支持以下的几个符号就基本够日常的应用了。
|--------+------------------------+--------------+-------------|
| Letter | Date or Time Component | Presentation | Examples |
|--------+------------------------+--------------+-------------|
| G | Era designator | Text | AD; BC |
|--------+------------------------+--------------+-------------|
| y | Year | Year | 2012; 97 |
|--------+------------------------+--------------+-------------|
| M | Month in year | Month | July;Jul;07 |
|--------+------------------------+--------------+-------------|
| d | Day in month | Number | 9; 09 |
|--------+------------------------+--------------+-------------|
| E | Day in week | Text | Tuesday;Tue |
|--------+------------------------+--------------+-------------|
| H | Hour in day (0-23) | Number | 0 |
|--------+------------------------+--------------+-------------|
| h | Hour in am/pm (1-12) | Number | 12 |
|--------+------------------------+--------------+-------------|
| m | Minute in hour | Number | 30 |
|--------+------------------------+--------------+-------------|
| s | Second in minute | Number | 59 |
|--------+------------------------+--------------+-------------|
| S | Millisecond | Number | 999 |
|--------+------------------------+--------------+-------------|
| a | AM/PM marker | Text | AM |
|--------+------------------------+--------------+-------------|
| z | Time Zone | Genral | CST |
|--------+------------------------+--------------+-------------|
| Z | Time Zone | RFC-822 | +0800 |
|--------+------------------------+--------------+-------------|
*** 2)和Java的SimpleDateFormat类似的使用方式
Java的SimpleDateFormat一般性的使用是非常简单的,常用的也就两个方法,就是format, parse。在javascript版本的SimpleDateFormat里,将会有以下的使用形式。
- #+begin_src javascript
- var sft = new js.text.SimpleDateFormat();
- sft.format(new Date()); // Format date to "Sun Mar 11 19:54:02 2012"
- var date = sft.parse("Sun Mar 11 19:54:02 2012"); // Parse date string
- sft.setPattern("yyyy年MM月dd日"); // Apply new pattern
- sft.format(date) // 2012年03月11日
- date = sft.parse("2012年03月11日");
- #+end_src
*** 3)提供多语言支持的接口DateFormatSymbols
尽管在javascript层面上处理多语言支持,似乎还不多见,但对于web应用逐步向编程化方向转换,直接在前端提供多语言的处理能力将会是非常有意义的。Java的SimpleDateFormat里,就使用了一个DateFormatSymbols的接口,从这个接口上就可以get到月份、星期、上午、下午等的多语言支持的符号。那么在javascript版本里,引入这个接口我们就可以实现如下的应用了。
- #+begin_src javascript
- var sft = new js.text.SimpleDateFormat("EEE MMM dd, yyyy", DateFormatSymbols);
- sft.format(new Date()); // Format date to "周日 三月 11, 2012"
- #+end_src
** 二、format和parse的实现设想
format和parse的功能都和一个东西有关,就是日期格式的pattern,而各种日期格式都可以用上面提到的几个模式符号(y, M, d, H ...)的排列来表示,如pattern
yyyy-MM-dd
就表示日期可以格式成“2012-03-08”。用正则表达式的思想来做,无非遍历这个模板,替换里面的模式符号,比如把yyyy,用日期的年来替换,等等。
而对于一个形如“2012年3月8日”的字符串,我们使用一个形如下面的pattern
yyyy年M月d日
也应该是可以parse出一个日期来的。比如按照模式符号和其位置,我们可以写出一个正则式来提取和日期有关的数值。比如写一个简单的能提取年、月、日的javascript的正则表达式,将会如下面这样:
- #+begin_src javascript
- var regx_date = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
- var m = "2012年3月11日".match(regx_date);
- #+end_src
当然上面这个正则式是有很多问题的,比如月份部分\d{1,2}将匹配0到99的数字,99这个数字对于月份来说显然是错误的,所以这部分还需要更复杂的表达式。
*** 1) 构建提取模式符号的正则表达式
我们需要从日期格式的pattern里提取模式符号,要提取的有:
* 两位数的年yy(还有人在用吗?), 四位数年yyyy。 正则式:/yy(?:yy)?/
* 月份M, M有一到四位的四钟可能。正则式:/M{1,4}/
* 日子d (Day in month),正则式: /d{1,2}/
* 星期E,一般有缩写和完整单词两种,正则式:/E{1,4}/
* 时分秒,是一位或两位数字,正则式:/([Hhms])\1?/
* 毫秒S,也有一位或三位的区别,正则式:/S(?:SS)?/
* 上下午a,时区z/Z,纪元G,正则式: /[azZG]/
到此,我们可以构建一个完整的正则表达式来提取模式符号了。
- #+begin_src javascript
- var TOKEN = /yy(?:yy)?|M{1,4}|d{1,2}|E{1,4}|([Hhms])\1?|S(?:SS)?|[azZG]/g;
- // 对于下面pattern
- var pattern = "EEE MMM dd, yyyy hh:mm:ss.SSS a Z";
- // 按TOKEN正则式来replace
- pattern.replace(TOKEN, function($0){
- //System.out.println($0); // 看看$0是什么?
- return $0;
- });
- #+end_src
*** 2) 构建用于parse的正则表达式
上面的正则表达式对于format已经足够了,但对于parse一个日期字符串来说,还要进一步构建可以提取年月日等数值的正则表达式。提取年月日等数值信息,首先需要提取两个信息:
* 位置信息,比如对于日期串“01/01/2012”来说,里面的两个“01”哪个是月份,哪
个是日子。
* 数值信息,比如对于日期串”Mar 11, 2012“,里面的“Mar”是三月份,对于ISO
格式的日期串"2012-03-11",三月份就是里面的“03”,如何提取到这些数值信息。
对于位置信息,根据前面提取的pattern中的模式符号,我们只要按找匹配到的顺序进行记录就可以了,比如:
- #+begin_src javascript
- var tIndex = [];
- // 对于下面pattern
- var pattern = "EEE MMM dd, yyyy hh:mm:ss.SSS a Z";
- // 按TOKEN正则式来replace
- pattern.replace(TOKEN, function($0){
-
- tIndex.push($0); // 按顺序记录模式符号的位置
-
- return $0;
- });
- #+end_src
对于数值信息,我们得先建立一张表,里面有yy, yyyy,M, MM, MMM, MMMM等各种模式的可能对应的数值的正则表达式,比如:
- #+begin_src javascript
- var regx = {
- yy : "(\\d{2})", // 2位数字
- yyyy : "(\\d{4})", // 4位数字
- M : "([1-9]|1[012])", // 1到9和10,11,12
- MM : "(0[1-9]|1[012])",// 01到12
- MMM : "(\\S+)", // 简单处理,非空白字符多个
- MMMM : "(\\S+)", // 简单处理,非空白字符多个
- d : "([1-9]|[12][0-9]|3[01])", // 1到31
- dd : "(0[1-9]|[12][0-9]|3[01])", // 01到31
- //....
- };
- #+end_src
然后,用查表发替换模式符号,比如“yyyy年MM月dd日”这个pattern可以替换成
yyyy年MM月dd日
|
|
v
(\\d{4})年(0[1-9]|1[012])月(0[1-9]|[12][0-9]|3[01])日
代码很好写,改造一下上面的方法:
- #+begin_src javascript
- var pattern = "yyyy年MM月dd日";
- // 按TOKEN正则式来replace
- var str = pattern.replace(TOKEN, function($0){
-
- tIndex.push($0); // 按顺序记录模式符号的位置
-
- // 查表获得模式符号的数值正则表达式
- if(typeof regx[$0] === "string"){
- return regx[$0];
- }
-
- return $0;
- });
- // 生成正则表达式
- var pRegx = new RegExp(str);
- #+end_src
*** 3) 实现上的一些设计
至此,实际上我们已经可以看到format和parse的初步样子了,无非是缺少一些如何get/set年月日等信息从(到)一个Date对象的体力活了。关于这部分如果按OO的思路来设计,程序虽然会略显臃肿,但会比较好维护。
对于format,我们可以造一个工具类叫Getter,里面有一堆方法,而方法名正好是模式符号,比如:
- #+begin_src javascript
- var Getter = new function(){
- this.yyyy = function(date, symbols){
- return date.getFullYear();
- };
- this.MMM = function(date, symbols){
- return symbols.getShortMonths()[date.getMonth()];
- };
- //...
- };
- #+end_src
而对于parse,也可以再造一个工具类叫Setter,里面同样有一堆以模式符号做为方法名的方法,比如:
- #+begin_src javascript
- var Setter = new function(){
- this.yyyy = function(date, value, symbols){
- date.setFullYear(value);
- return date;
- };
- this.MMM = function(date, value, symbols){
- var i = symbols.getShortMonths().indexOf(value);
- date.setMonth(i);
- return i;
- };
- //...
- };
- #+end_src
那么SimpleDateFormat的format和parse方法的实现就会显得很优雅了。
- #+begin_src javascript
- var SimpleDateFormat = function(pattern, symbols){
- this.format = function(date){
- var datestr = pattern.replace(TOKEN, function($0){
- return Getter[$0](date, symbols);
- });
- return datestr;
- };
- this.parse = function(datestr){
-
- var m = datestr.match(pRegx), $0,
- date = new Date();
- for(var i=1, len=m.length; i<len; i++){
- $0 = tIndex[i-1]; // 从符号顺序表中获得模式符号
-
- date = Setter[$0](date, m[i] ,symbols);
- }
-
- return date;
- };
- };
- #+end_src
** 三、后记
不要pattern可以parse日期时间吗? 是不是要收集足够多的pattern,然后一个一个测试能否parse出Date对象来?
以上的代码在一般情况下是可以工作的,但用于生产环境的话,还需要做一些例外和出错时的处理,具体的就不在这篇技术文章中写了。有兴趣了解的可以到我的开源项目 去看正式的源代码,主要是js.text.SimpleDateFormat。
阅读(2895) | 评论(0) | 转发(0) |