UNPKG

@telerik/kendo-intl

Version:

A package exporting functions for date and number parsing and formatting

590 lines (474 loc) 14.7 kB
import { adjustDST, convertTimeZone } from './time-utils'; import { localeInfo } from '../cldr'; import { DEFAULT_LOCALE, EMPTY } from '../common/constants'; import { errors } from '../errors'; import formatNames from './format-names'; import datePattern from './date-pattern'; import round from '../common/round'; import isDate from '../common/is-date'; const timeZoneOffsetRegExp = /([+|\-]\d{1,2})(:?)(\d{2})?/; const dateRegExp = /^\/Date\((.*?)\)\/$/; const offsetRegExp = /[+-]\d*/; const numberRegExp = { 2: /^\d{1,2}/, 3: /^\d{1,3}/, 4: /^\d{4}/ }; const numberRegex = /\d+/; const PLACEHOLDER = "{0}"; const leadingSpacesRegex = /^ */; const trailingSpacesRegex = / *$/; const standardDateFormats = [ "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM/dd", "E MMM dd yyyy HH:mm:ss", "yyyy-MM-ddTHH:mm:ss.SSSSSSSXXX", "yyyy-MM-ddTHH:mm:ss.SSSXXX", "yyyy-MM-ddTHH:mm:ss.SSXXX", "yyyy-MM-ddTHH:mm:ssXXX", "yyyy-MM-ddTHH:mm:ss.SSSSSSS", "yyyy-MM-ddTHH:mm:ss.SSS", "yyyy-MM-ddTHH:mmXXX", "yyyy-MM-ddTHH:mmX", "yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM-dd", "HH:mm:ss", "HH:mm" ]; const FORMATS_SEQUENCE = [ "G", "g", "F", "Y", "y", "M", "m", "D", "d", "y", "T", "t" ]; const TWO_DIGIT_YEAR_MAX = 2029; function outOfRange(value, start, end) { return !(value >= start && value <= end); } function lookAhead(match, state) { let { format, idx } = state; let i = 0; while (format[idx] === match) { i++; idx++; } if (i > 0) { idx -= 1; } state.idx = idx; return i; } function getNumber(size, state) { let regex = size ? (numberRegExp[size] || new RegExp('^\\d{1,' + size + '}')) : numberRegex, match = state.value.substr(state.valueIdx, size).match(regex); if (match) { match = match[0]; state.valueIdx += match.length; return parseInt(match, 10); } return null; } function getIndexByName(names, state, lower) { let i = 0, length = names.length, name, nameLength, matchLength = 0, matchIdx = 0, subValue; for (; i < length; i++) { name = names[i]; nameLength = name.length; subValue = state.value.substr(state.valueIdx, nameLength); if (lower) { subValue = subValue.toLowerCase(); } if (subValue === name && nameLength > matchLength) { matchLength = nameLength; matchIdx = i; } } if (matchLength) { state.valueIdx += matchLength; return matchIdx + 1; } return null; } function checkLiteral(state) { let result = false; if (state.value.charAt(state.valueIdx) === state.format[state.idx]) { state.valueIdx++; result = true; } return result; } function calendarGmtFormats(calendar) { const { gmtFormat, gmtZeroFormat } = calendar; if (!gmtFormat) { throw errors.NoGMTInfo.error(); } return [ gmtFormat.replace(PLACEHOLDER, EMPTY).toLowerCase(), gmtZeroFormat.replace(PLACEHOLDER, EMPTY).toLowerCase() ]; } function parseTimeZoneOffset(state, info, options) { const { shortHours, noSeparator, optionalMinutes, localizedName, zLiteral } = options; state.UTC = true; if (zLiteral && state.value.charAt(state.valueIdx) === "Z") { state.valueIdx++; return false; } if (localizedName && !getIndexByName(calendarGmtFormats(info.calendar), state, true)) { return true; } const matches = timeZoneOffsetRegExp.exec(state.value.substr(state.valueIdx, 6)); if (!matches) { return !localizedName; } const hoursMatch = matches[1]; const minutesMatch = matches[3]; const hoursOffset = parseInt(hoursMatch, 10); const separator = matches[2]; let minutesOffset = parseInt(minutesMatch, 10); if (isNaN(hoursOffset) || (!shortHours && hoursMatch.length !== 3) || (!optionalMinutes && isNaN(minutesOffset)) || (noSeparator && separator)) { return true; } if (isNaN(minutesOffset)) { minutesOffset = null; } if (outOfRange(hoursOffset, -12, 13) || (minutesOffset && outOfRange(minutesOffset, 0, 59))) { return true; } state.valueIdx += matches[0].length; state.hoursOffset = hoursOffset; state.minutesOffset = minutesOffset; } function parseMonth(ch, state, info) { const count = lookAhead(ch, state); const names = formatNames(info, "months", count, ch === "L", true); const month = count < 3 ? getNumber(2, state) : getIndexByName(names, state, true); if (month === null || outOfRange(month, 1, 12)) { return true; } state.month = month - 1; } function parseDayOfWeek(ch, state, info) { const count = lookAhead(ch, state); const names = formatNames(info, "days", count, ch === "c", true); let dayOfWeek = count < 3 ? getNumber(1, state) : getIndexByName(names, state, true); if ((!dayOfWeek && dayOfWeek !== 0) || outOfRange(dayOfWeek, 1, 7)) { return true; } } const parsers = {}; parsers.d = function(state) { lookAhead("d", state); const day = getNumber(2, state); if (day === null || outOfRange(day, 1, 31)) { return true; } if (state.day === null) { state.day = day; } }; parsers.E = function(state, info) { const count = lookAhead("E", state); //validate if it matches the day? let dayOfWeek = getIndexByName(formatNames(info, "days", count, false, true), state, true); if (dayOfWeek === null) { return true; } }; parsers.M = function(state, info) { return parseMonth("M", state, info); }; parsers.L = function(state, info) { return parseMonth("L", state, info); }; parsers.y = function(state) { const count = lookAhead("y", state); let year = getNumber(count === 1 ? undefined : count, state); if (year === null) { return true; } if (count === 2) { const currentYear = new Date().getFullYear(); year = (currentYear - currentYear % 100) + year; if (year > TWO_DIGIT_YEAR_MAX) { year -= 100; } } state.year = year; }; parsers.h = function(state) { lookAhead("h", state); let hours = getNumber(2, state); if (hours === 12) { hours = 0; } if (hours === null || outOfRange(hours, 0, 11)) { return true; } state.hours = hours; }; parsers.K = function(state) { lookAhead("K", state); let hours = getNumber(2, state); if (hours === null || outOfRange(hours, 0, 11)) { return true; } state.hours = hours; }; parsers.a = function(state, info) { const count = lookAhead("a", state); let periodFormats = formatNames(info, "dayPeriods", count, false, true); const pmHour = getIndexByName([ periodFormats.pm ], state, true); if (!pmHour && !getIndexByName([ periodFormats.am ], state, true)) { return true; } state.pmHour = pmHour; }; parsers.H = function(state) { lookAhead("H", state); const hours = getNumber(2, state); if (hours === null || outOfRange(hours, 0, 23)) { return true; } state.hours = hours; }; parsers.k = function(state) { lookAhead("k", state); let hours = getNumber(2, state); if (hours === null || outOfRange(hours, 1, 24)) { return true; } state.hours = hours === 24 ? 0 : hours; }; parsers.m = function(state) { lookAhead("m", state); const minutes = getNumber(2, state); if (minutes === null || outOfRange(minutes, 0, 59)) { return true; } state.minutes = minutes; }; parsers.s = function(state) { lookAhead("s", state); const seconds = getNumber(2, state); if (seconds === null || outOfRange(seconds, 0, 59)) { return true; } state.seconds = seconds; }; parsers.S = function(state) { const count = lookAhead("S", state); const match = state.value.substr(state.valueIdx, count); let milliseconds = null; if (!isNaN(parseInt(match, 10))) { milliseconds = parseFloat("0." + match, 10); milliseconds = round(milliseconds, 3); milliseconds *= 1000; state.valueIdx += count; } if (milliseconds === null || outOfRange(milliseconds, 0, 999)) { return true; } state.milliseconds = milliseconds; }; parsers.z = function(state, info) { const count = lookAhead("z", state); const shortFormat = count < 4; const invalid = parseTimeZoneOffset(state, info, { shortHours: shortFormat, optionalMinutes: shortFormat, localizedName: true }); if (invalid) { return invalid; } }; parsers.Z = function(state, info) { const count = lookAhead("Z", state); const invalid = parseTimeZoneOffset(state, info, { noSeparator: count < 4, zLiteral: count === 5, localizedName: count === 4 }); if (invalid) { return invalid; } }; parsers.x = function(state, info) { const count = lookAhead("x", state); const invalid = parseTimeZoneOffset(state, info, { noSeparator: count !== 3 && count !== 5, optionalMinutes: count === 1 }); if (invalid) { return invalid; } }; parsers.X = function(state, info) { const count = lookAhead("X", state); const invalid = parseTimeZoneOffset(state, info, { noSeparator: count !== 3 && count !== 5, optionalMinutes: count === 1, zLiteral: true }); if (invalid) { return invalid; } }; parsers.G = function(state, info) { const count = lookAhead("G", state); const eras = formatNames(info, "eras", count, false, true); const era = getIndexByName([ eras[0], eras[1] ], state, true); if (era === null) { return true; } }; parsers.e = function(state, info) { return parseDayOfWeek("e", state, info); }; parsers.c = function(state, info) { return parseDayOfWeek("c", state, info); }; function createDate(state) { let { year, month, day, hours, minutes, seconds, milliseconds, pmHour, UTC, hoursOffset, minutesOffset } = state; const hasTime = hours !== null || minutes !== null || seconds || null; const date = new Date(); let result; if (year === null && month === null && day === null && hasTime) { year = date.getFullYear(); month = date.getMonth(); day = date.getDate(); } else { if (year === null) { year = date.getFullYear(); } if (day === null) { day = 1; } } if (pmHour && hours < 12) { hours += 12; } if (UTC) { if (hoursOffset) { hours += -hoursOffset; } if (minutesOffset) { minutes += -minutesOffset * (hoursOffset < 0 ? -1 : 1); } result = new Date(Date.UTC(year, month, day, hours, minutes, seconds, milliseconds)); } else { result = new Date(year, month, day, hours, minutes, seconds, milliseconds); adjustDST(result, hours); } if (year < 100) { result.setFullYear(year); } if (result.getDate() !== day && UTC === undefined) { return null; } return result; } function addFormatSpaces(value, format) { const leadingSpaces = leadingSpacesRegex.exec(format)[0]; const trailingSpaces = trailingSpacesRegex.exec(format)[0]; return `${ leadingSpaces }${ value }${ trailingSpaces }`; } function parseExact(value, format, info) { let pattern = datePattern(format, info).split(EMPTY); const state = { format: pattern, idx: 0, value: addFormatSpaces(value, format), valueIdx: 0, year: null, month: null, day: null, hours: null, minutes: null, seconds: null, milliseconds: null }; const length = pattern.length; let literal = false; for (; state.idx < length; state.idx++) { let ch = pattern[state.idx]; if (literal) { if (ch === "'") { literal = false; } checkLiteral(state); } else { if (parsers[ch]) { let invalid = parsers[ch](state, info); if (invalid) { return null; } } else if (ch === "'") { literal = true; checkLiteral(state); } else if (!checkLiteral(state)) { return null; } } } if (state.valueIdx < value.length) { return null; } return createDate(state) || null; } function parseMicrosoftDateOffset(offset) { const sign = offset.substr(0, 1) === "-" ? -1 : 1; let result = offset.substring(1); result = (parseInt(result.substr(0, 2), 10) * 60) + parseInt(result.substring(2), 10); return sign * result; } function parseMicrosoftDateFormat(value) { if (value && value.indexOf("/D") === 0) { let date = dateRegExp.exec(value); if (date) { date = date[1]; let tzoffset = offsetRegExp.exec(date.substring(1)); date = new Date(parseInt(date, 10)); if (tzoffset) { tzoffset = parseMicrosoftDateOffset(tzoffset[0]); date = convertTimeZone(date, date.getTimezoneOffset(), 0); date = convertTimeZone(date, 0, -1 * tzoffset); } return date; } } } function defaultFormats(calendar) { const formats = []; const patterns = calendar.patterns; const length = FORMATS_SEQUENCE.length; for (let idx = 0; idx < length; idx++) { formats.push(patterns[FORMATS_SEQUENCE[idx]]); } return formats.concat(standardDateFormats); } export default function parseDate(value, formats, locale = DEFAULT_LOCALE) { if (!value) { return null; } if (isDate(value)) { return value; } let parseValue = String(value).trim(); let date = parseMicrosoftDateFormat(parseValue); if (date) { return date; } const info = localeInfo(locale); let parseFormats = formats || defaultFormats(info.calendar); parseFormats = Array.isArray(parseFormats) ? parseFormats : [ parseFormats ]; const length = parseFormats.length; for (let idx = 0; idx < length; idx++) { date = parseExact(parseValue, parseFormats[idx], info); if (date) { return date; } } return date; }