使用过moment这个库的同学应该对它的格式化功能非常喜欢。但是,我们不可能为了一个格式化功能,而引入整个moment库。这篇文章,提供了一个没有依赖的日期格式工具。
识别日期格式
这里所提的日期,主要是指字符串形式的日期格式。我们通常的情况是,想将某种字符串形式的日期,转化为其他格式的日期,例如,将2019/12/20转化为2019-12-20这种格式。如果单纯考虑是字符串,直接替换/为-即可,干嘛那么麻烦?但是,由于不同系统中可能需要适应具体的格式场景,所以,就需要一个日期的格式切换工具。但,问题在于,如何识别原日期格式呢?举个例子,05/06到底是指5月6日,还是指6月5日呢?如果没有辅助信息,我们无从判定。
通过givenFormatter来判别它到底是什么格式,如果你传了MM/DD,那么我就可以知道,这是5月6日。因此,我们的首要任务,是可以通过givenFormatter来识别原有的日期格式。
既然是formatter,那么,我们就要定制一个规则,不是用户自己想怎么传就可以怎么传的。参照moment的格式,我做了如下规定:
export const DATE_MONTHS = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ] export const DATE_WEEKS = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', ] export const DATE_EXPS = { YYYY: '[12][0-9]{3}', YY: '[0-9]{2}', M: '[1-9]|1[0-2]', MM: '0[1-9]|1[0-2]', MMM: DATE_MONTHS.join('|'), D: '[1-9]|[1-2][0-9]|3[0-1]', DD: '[0-2][0-9]|3[0-1]', W: '[0-6]', WWW: DATE_WEEKS.join('|'), H: '[0-9]|1[0-9]|2[0-3]', HH: '[01][0-9]|2[0-3]', h: '[0-9]|1[0-2]', hh: '0[0-9]|1[0-2]', a: 'am|pm', A: 'AM|PM', m: '[0-9]|[1-5][0-9]', mm: '[0-5][0-9]', s: '[0-9]|[1-5][0-9]', ss: '[0-5][0-9]', S: '[0-9]|[1-9][0-9]|[1-9][0-9]{2}', SSS: '[0-9]{3}', }
你可以很容易看出,这些配置是为正则表达式准备的。我们要识别传入的日期是什么格式,肯定需要借助正则表达式的能力。这做了如上规定之后,我们就可以对用户传入的日期格式进行解析了,得到每一个位置上的值。例如:
用户传入:2014/05/06 8:43 am 给定格式:YYYY/DD/MM h:m a
那么经过解析之后,我可以得到一个对象:
{ YYYY: '2014', DD: '05', MM: '06', h: '8', m: '43', a: 'am', }
通过这些信息,我再用Date得到一个时间戳。
const date = new Date(2014, 5, 5, 8, 43, 00)
有了这个date,取什么数据都可以取得到了。现在,我们得到了解析后的日期,把它放在这里备用。
转化日期格式
上面,我们已经得到一个经过解析后的日期对象。我们已经顺利拿到了用户给的日期。现在,我们要将日期进行格式化,并输出为用户期望的格式。
export const DATE_FORMATTERS = { YYYY: date => date.getFullYear() + '', YY: date => (date.getFullYear() % 100) + '', M: date => (date.getMonth() + 1) + '', MM: date => pad(date.getMonth() + 1), MMM: date => DATE_MONTHS[date.getMonth()], D: date => date.getDate() + '', DD: date => pad(date.getDate()), W: date => date.getDay() + '', WWW: date => DATE_WEEKS[date.getDay()], H: date => date.getHours() + '', HH: date => pad(date.getHours()), h: date => date.getHours() % 12 + '', hh: date => pad(date.getHours() % 12), a: date => date.getHours() < 12 ? 'am' : 'pm', A: date => date.getHours() < 12 ? 'AM' : 'PM', m: date => date.getMinutes() + '', mm: date => pad(date.getMinutes()), s: date => date.getSeconds() + '', ss: date => pad(date.getSeconds()), SSS: date => date.getMilliseconds() + '', }
我定制了一个集合,用于获取每一个格式它需要输出日期的什么内容。但是,核心是我们有了date,也得到了用户给我们的目标日期格式,但是,我们怎么保持用户给定格式中的噪声(例如下面时间中的冒号:)?
用户期望:WWW MMM DD YYYY HH:mm:ss
我们如何确保按照这个格式输出用户想要的结果呢?显然,遍历字符串是一个笨方法。我们这里用到了正则表达式的能力:
const output = formatter.replace(reg, (matched, found) => { return DATE_FORMATTERS[found](date) })
这段代码里的reg则是根据DATE_FORMATTERS的键名来连接的。
const keys = Object.keys(DATE_FORMATTERS) const reg = new RegExp('(' + keys.join('|') + ')', 'g')
通过这种方式,我们替换掉了给定的格式里面的对应的值,这解决了我们保留噪声的需要。
解决日期创建时的问题
在上面得到date时,会有一些小问题。在js自带的日期系统中,我们有两个地方需要做硬性的规定,一个是月份的地方,一个是小时的声音。用户在给出一个时间格式的时候,月份有可能是数字的形式,但也有可能是Jan这种字符形式。小时有可能是12小时制也可能是24小时制。怎么解决这个问题呢?
实际上,只需要几个简单的判定即可。
const Y = +(parsedDate.YYYY || ('20' + parsedDate.YY)) || (new Date().getFullYear()) const D = +(parsedDate.DD || parsedDate.D) || 1 const m = +(parsedDate.mm || parsedDate.m) || 0 const s = +(parsedDate.ss || parsedDate.s) || 0 var M if (parsedDate.MM || parsedDate.M) { M = +(parsedDate.MM || parsedDate.M) - 1 || 0 } else if (parsedDate.MMM) { let m = parsedDate.MMM let i = DATE_MONTHS.indexOf(m) i = i === - 1 ? 0 : i M = i } else { M = 0 } var H if (parsedDate.HH || parsedDate.H) { H = +(parsedDate.HH || parsedDate.H) || 0 } else if (parsedDate.hh || parsedDate.h) { let a = (parsedDate.a || parsedDate.A || 'am').toLowerCase() let h = +(parsedDate.hh || parsedDate.h) || 0 H = a === 'pm' ? h + 12 : h } else { H = 0 } const date = new Date(Y, M, D, H, m, s)
保留关键字噪声
用户提供:YY-MM-DD h:m:s S\\m\\s
上面的格式字符串中\\m\\s最后得到的是ms这两个字母。那这个又是怎么去实现呢?
第一步是在对用户提供的格式字符串中进行转义的识别,那些被转义的部分,不进行格式识别转化。
const parserKeys = getFormatterKeys()
const formatterExp = '(?<!\\\\)(' + parserKeys.join('|') + ')'
const formatterReg = new RegExp(formatterExp, 'g')
上面红色的部分是正则断言的一种方式,在正则匹配时,将不会去匹配前面有\\的关键字。
第二步是在输出结果时,将\\去掉。
const parserKeys = getFormatterKeys() const formatterExp = '\\\\\\\\(' + parserKeys.join('|') + ')' const formatterReg = new RegExp(formatterExp, 'g') const res = output.replace(formatterReg, (matched, found) => { return found })
这样就可以替换掉\\,直接输出字母。
结语
本文详细解释了如何实现js日期格式转化的思路。你可以通过这里阅读源码。其中非常重要的就是利用String.replace和RegExp配合,在replace的第二个参数传入函数来进行处理,达到格式化中保持原有噪声的效果。如果你对本文中描述的内容有问题,可以通过下方评论框和我讨论。