import { isFunction, isInstanceOf, isNumber, isNumeric, isString } from './is.js'
import { padRight, padLeft } from './string.js'
import { createArray } from './array.js'
export function getDateForamtterConfigs() {
if (getDateForamtterConfigs.value) {
return getDateForamtterConfigs.value
}
const pad = num => num < 10 ? '0' + num : num + ''
const DATE_MONTHS = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
]
const DATE_WEEKS = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thur',
'Fri',
'Sat',
]
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}',
}
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()),
S: date => date.getMilliseconds() + '',
SSS: date => padLeft(date.getMilliseconds() + '', 3, '0'),
}
const configs = {
DATE_MONTHS,
DATE_WEEKS,
DATE_EXPS,
DATE_FORMATTERS
}
getDateForamtterConfigs.value = configs
return configs
}
function getFormatterKeys() {
const { DATE_EXPS } = getDateForamtterConfigs()
const parserKeys = Object.keys(DATE_EXPS)
parserKeys.sort()
parserKeys.reverse()
return parserKeys
}
// const ensureRegExpString = (formatter) => {
// const sign = '*.?+$^[](){}|\\/'
// const signArr = sign.split('')
// const formatterArr = formatter.split('')
// const formatterList = formatterArr.map(char => signArr.indexOf(char) > -1 ? '\\' + char : char)
// const formatterStr = formatterList.join('')
// return formatterStr
// }
// convert 'a' to unicode '\uaaa0', 'A' to '\uaaa1'
function convertCharToUnicode(char) {
const lower = char.toLowerCase()
const isLower = char === lower
const end = isLower ? 0 : 1
const arr = createArray(lower, 3)
const str = arr.join('') + end
const unicode = '\\u' + str
const obj = JSON.parse(`["${unicode}"]`)
const unichar = obj[0]
return unichar
}
function convertUnicodeToChar(unichar) {
const code = unichar.charCodeAt(0)
const unicode = code.toString(16)
const char = unicode.substr(0, 1)
const end = unicode.substr(-1)
const letter = end === '1' ? char.toUpperCase() : char
return letter
}
function parseFormatter(formatter, fn) {
const parserKeys = getFormatterKeys()
// the following code allow use to use formatter like `YY-MM\\M`, the last `\\M` will turn out to be `M` in the final output string
const replaceReg = new RegExp('\\\\(' + parserKeys.join('|') + ')', 'g')
const replacedChars = []
const preFormatted = formatter.replace(replaceReg, (matched, letter) => {
const unichar = convertCharToUnicode(letter)
replacedChars.push(unichar)
return unichar
})
const formatterReg = new RegExp('(' + parserKeys.join('|') + ')', 'g')
const afterFormatted = preFormatted.replace(formatterReg, (matched, found) => {
if (isFunction(fn)) {
return fn(found)
}
return found
})
let output = afterFormatted
for (let i = 0, len = replacedChars.length; i < len; i ++) {
const letter = replacedChars[i]
const char = convertUnicodeToChar(letter)
output = output.replace(letter, char)
}
return output
}
function parseDate(dateString, formatter) {
const { DATE_EXPS } = getDateForamtterConfigs()
const foundParsers = []
const dateExp = parseFormatter(formatter, (found) => {
foundParsers.push(found)
return '(' + DATE_EXPS[found] + ')'
})
const dateReg = new RegExp(dateExp)
if (!dateReg.test(dateString)) {
return null
}
const dateFound = []
dateString.replace(dateReg, (matched, ...founds) => {
dateFound.push(...founds)
})
const dateRes = {}
foundParsers.forEach((key, i) => {
if (dateRes[key]) {
return
}
dateRes[key] = dateFound[i]
})
return dateRes
}
function parseFormalDate(dateString) {
const [date, time = ''] = dateString.split(' ')
const [Y, M = '01', D = '01'] = date.split('-')
const [hms = '', ms = '000'] = time.split('.')
const [H = '00', m = '00', s = '00'] = hms.split(':')
const sss = padRight(ms, 3, '0')
return [+Y, +M - 1, +D, +H, +m, +s, +sss]
}
/**
* @param {Date|string|number} datetime
* @param {string} [givenFormatter]
* @returns {Date}
*/
export function createDate(datetime, givenFormatter) {
if (isInstanceOf(datetime, Date)) {
return datetime
}
if (isNumber(datetime)) {
return new Date(datetime)
}
if (isNumeric(datetime)) {
return new Date(+datetime)
}
if (!isString(datetime)) {
return new Date()
}
if (!givenFormatter) {
const items = parseFormalDate(datetime)
return new Date(...items)
}
const parsedDate = parseDate(datetime, givenFormatter)
if (!parsedDate) {
const items = parseFormalDate(datetime)
return new Date(...items)
}
const { DATE_MONTHS } = getDateForamtterConfigs()
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
let M = 0
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
}
let H = 0
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
}
let ms = 0
if (parsedDate.SSS) {
ms = +parsedDate.SSS
}
else if (parsedDate.S) {
ms = +padRight(parsedDate.S, 3, '0')
}
return new Date(Y, M, D, H, m, s, ms)
}
/**
* @param {Date|string|number} datetime
* @param {string} formatter
* @param {string} [givenFormatter]
* @returns {string}
*/
export function formatDate(datetime, formatter, givenFormatter) {
if (!datetime) {
return
}
if (!formatter) {
return
}
const { DATE_FORMATTERS } = getDateForamtterConfigs()
const date = createDate(datetime, givenFormatter)
const output = parseFormatter(formatter, (found) => {
const format = DATE_FORMATTERS[found]
if (format) {
return format(date)
}
else {
return found
}
})
// the following code ensure the `\\M` to be `M` in formatter
const parserKeys = getFormatterKeys()
const formatterExp = '\\\\(' + parserKeys.join('|') + ')'
const formatterReg = new RegExp(formatterExp, 'g')
const res = output.replace(formatterReg, (matched, found) => {
return found
})
return res
}