UNPKG

datamodel

Version:

Relational algebra compliant in-memory tabular data store

752 lines (671 loc) 22.9 kB
/** * Creates a JS native date object from input * * @param {string | number | Date} date Input using which date object to be created * @return {Date} : JS native date object */ function convertToNativeDate (date) { if (date instanceof Date) { return date; } return new Date(date); } /** * Apply padding before a number if its less than 1o. This is used when constant digit's number to be returned * between 0 - 99 * * @param {number} n Input to be padded * @return {string} Padded number */ function pad (n) { return (n < 10) ? (`0${n}`) : n; } /* * DateFormatter utility to convert any date format to any other date format * DateFormatter parse a date time stamp specified by a user abiding by rules which are defined * by user in terms of token. It creates JS native date object from the user specified format. * That native date can also be displayed * in any specified format. * This utility class only takes care of format conversion only */ /* * Escapes all the special character that are used in regular expression. * Like * RegExp.escape('sgfd-$') // Output: sgfd\-\$ * * @param text {String} : text which is to be escaped */ RegExp.escape = function (text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); }; /** * DateTimeFormatter class to convert any user format of date time stamp to any other format * of date time stamp. * * @param {string} format Format of the date given. For the above date, * 'year: %Y, month: %b, day: %d'. * @class */ /* istanbul ignore next */ function DateTimeFormatter (format) { this.format = format; this.dtParams = undefined; this.nativeDate = undefined; } // The identifier of the tokens DateTimeFormatter.TOKEN_PREFIX = '%'; // JS native Date constructor takes the date params (year, month, etc) in a certail sequence. // This defines the sequence of the date parameters in the constructor. DateTimeFormatter.DATETIME_PARAM_SEQUENCE = { YEAR: 0, MONTH: 1, DAY: 2, HOUR: 3, MINUTE: 4, SECOND: 5, MILLISECOND: 6 }; /* * This is a default number parsing utility. It tries to parse a number in integer, if parsing is unsuccessful, it * gives back a default value. * * @param: defVal {Number} : Default no if the parsing to integer is not successful * @return {Function} : An closure function which is to be called by passing an the value which needs to be parsed. */ DateTimeFormatter.defaultNumberParser = function (defVal) { return function (val) { let parsedVal; if (isFinite(parsedVal = parseInt(val, 10))) { return parsedVal; } return defVal; }; }; /* * This is a default number range utility. It tries to find an element in the range. If not found it returns a * default no as an index. * * @param: range {Array} : The list which is to be serached * @param: defVal {Number} : Default no if the serach and find does not return anything * @return {Function} : An closure function which is to be called by passing an the value which needs to be found */ DateTimeFormatter.defaultRangeParser = function (range, defVal) { return (val) => { let i; let l; if (!val) { return defVal; } const nVal = val.toLowerCase(); for (i = 0, l = range.length; i < l; i++) { if (range[i].toLowerCase() === nVal) { return i; } } if (i === undefined) { return defVal; } return null; }; }; /* * Defines the tokens which are supporter by the dateformatter. Using this definitation a value gets extracted from * the user specifed date string. This also formats the value for display purpose from native JS date. * The definition of each token contains the following named properties * { * %token_name% : { * name: name of the token, this is used in reverse lookup, * extract: a function that returns the regular expression to extract that piece of information. All the * regex should be gouped by using () * parser: a function which receives value extracted by the above regex and parse it to get the date params * formatter: a formatter function that takes milliseconds or JS Date object and format the param * represented by the token only. * } * } * * @return {Object} : Definition of the all the supported tokens. */ DateTimeFormatter.getTokenDefinitions = function () { const daysDef = { short: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], long: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ] }; const monthsDef = { short: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ], long: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ] }; const definitions = { H: { // 24 hours format name: 'H', index: 3, extract () { return '(\\d+)'; }, parser: DateTimeFormatter.defaultNumberParser(), formatter (val) { const d = convertToNativeDate(val); return d.getHours().toString(); } }, l: { // 12 hours format name: 'l', index: 3, extract () { return '(\\d+)'; }, parser: DateTimeFormatter.defaultNumberParser(), formatter (val) { const d = convertToNativeDate(val); const hours = d.getHours() % 12; return (hours === 0 ? 12 : hours).toString(); } }, p: { // AM or PM name: 'p', index: 3, extract () { return '(AM|PM)'; }, parser: (val) => { if (val) { return val.toLowerCase(); } return null; }, formatter: (val) => { const d = convertToNativeDate(val); const hours = d.getHours(); return (hours < 12 ? 'AM' : 'PM'); } }, P: { // am or pm name: 'P', index: 3, extract () { return '(am|pm)'; }, parser: (val) => { if (val) { return val.toLowerCase(); } return null; }, formatter: (val) => { const d = convertToNativeDate(val); const hours = d.getHours(); return (hours < 12 ? 'am' : 'pm'); } }, M: { // Two digit minutes 00 - 59 name: 'M', index: 4, extract () { return '(\\d+)'; }, parser: DateTimeFormatter.defaultNumberParser(), formatter (val) { const d = convertToNativeDate(val); const mins = d.getMinutes(); return pad(mins); } }, S: { // Two digit seconds 00 - 59 name: 'S', index: 5, extract () { return '(\\d+)'; }, parser: DateTimeFormatter.defaultNumberParser(), formatter (val) { const d = convertToNativeDate(val); const seconds = d.getSeconds(); return pad(seconds); } }, K: { // Milliseconds name: 'K', index: 6, extract () { return '(\\d+)'; }, parser: DateTimeFormatter.defaultNumberParser(), formatter (val) { const d = convertToNativeDate(val); const ms = d.getMilliseconds(); return ms.toString(); } }, a: { // Short name of day, like Mon name: 'a', index: 2, extract () { return `(${daysDef.short.join('|')})`; }, parser: DateTimeFormatter.defaultRangeParser(daysDef.short), formatter (val) { const d = convertToNativeDate(val); const day = d.getDay(); return (daysDef.short[day]).toString(); } }, A: { // Long name of day, like Monday name: 'A', index: 2, extract () { return `(${daysDef.long.join('|')})`; }, parser: DateTimeFormatter.defaultRangeParser(daysDef.long), formatter (val) { const d = convertToNativeDate(val); const day = d.getDay(); return (daysDef.long[day]).toString(); } }, e: { // 8 of March, 11 of November name: 'e', index: 2, extract () { return '(\\d+)'; }, parser: DateTimeFormatter.defaultNumberParser(), formatter (val) { const d = convertToNativeDate(val); const day = d.getDate(); return day.toString(); } }, d: { // 08 of March, 11 of November name: 'd', index: 2, extract () { return '(\\d+)'; }, parser: DateTimeFormatter.defaultNumberParser(), formatter (val) { const d = convertToNativeDate(val); const day = d.getDate(); return pad(day); } }, b: { // Short month, like Jan name: 'b', index: 1, extract () { return `(${monthsDef.short.join('|')})`; }, parser: DateTimeFormatter.defaultRangeParser(monthsDef.short), formatter (val) { const d = convertToNativeDate(val); const month = d.getMonth(); return (monthsDef.short[month]).toString(); } }, B: { // Long month, like January name: 'B', index: 1, extract () { return `(${monthsDef.long.join('|')})`; }, parser: DateTimeFormatter.defaultRangeParser(monthsDef.long), formatter (val) { const d = convertToNativeDate(val); const month = d.getMonth(); return (monthsDef.long[month]).toString(); } }, m: { // Two digit month of year like 01 for January name: 'm', index: 1, extract () { return '(\\d+)'; }, parser (val) { return DateTimeFormatter.defaultNumberParser()(val) - 1; }, formatter (val) { const d = convertToNativeDate(val); const month = d.getMonth(); return pad(month + 1); } }, y: { // Short year like 90 for 1990 name: 'y', index: 0, extract () { return '(\\d{2})'; }, parser (val) { let result; if (val) { const l = val.length; val = val.substring(l - 2, l); } let parsedVal = DateTimeFormatter.defaultNumberParser()(val); let presentDate = new Date(); let presentYear = Math.trunc((presentDate.getFullYear()) / 100); result = `${presentYear}${parsedVal}`; if (convertToNativeDate(result).getFullYear() > presentDate.getFullYear()) { result = `${presentYear - 1}${parsedVal}`; } return convertToNativeDate(result).getFullYear(); }, formatter (val) { const d = convertToNativeDate(val); let year = d.getFullYear().toString(); let l; if (year) { l = year.length; year = year.substring(l - 2, l); } return year; } }, Y: { // Long year like 1990 name: 'Y', index: 0, extract () { return '(\\d{4})'; }, parser: DateTimeFormatter.defaultNumberParser(), formatter (val) { const d = convertToNativeDate(val); const year = d.getFullYear().toString(); return year; } } }; return definitions; }; /* * The tokens which works internally is not user friendly in terms of memorizing the names. This gives a formal * definition to the informal notations. * * @return {Object} : Formal definition of the tokens */ DateTimeFormatter.getTokenFormalNames = function () { const definitions = DateTimeFormatter.getTokenDefinitions(); return { HOUR: definitions.H, HOUR_12: definitions.l, AMPM_UPPERCASE: definitions.p, AMPM_LOWERCASE: definitions.P, MINUTE: definitions.M, SECOND: definitions.S, SHORT_DAY: definitions.a, LONG_DAY: definitions.A, DAY_OF_MONTH: definitions.e, DAY_OF_MONTH_CONSTANT_WIDTH: definitions.d, SHORT_MONTH: definitions.b, LONG_MONTH: definitions.B, MONTH_OF_YEAR: definitions.m, SHORT_YEAR: definitions.y, LONG_YEAR: definitions.Y }; }; /* * This defines the rules and declares dependencies that resolves a date parameter (year, month etc) from * the date time parameter array. * * @return {Object} : An object that contains dependencies and a resolver function. The dependencies values are fed * to the resolver function in that particular sequence only. */ DateTimeFormatter.tokenResolver = function () { const definitions = DateTimeFormatter.getTokenDefinitions(); const defaultResolver = (...args) => { // eslint-disable-line require-jsdoc let i = 0; let arg; let targetParam; const l = args.length; for (; i < l; i++) { arg = args[i]; if (args[i]) { targetParam = arg; } } if (!targetParam) { return null; } return targetParam[0].parser(targetParam[1]); }; return { YEAR: [definitions.y, definitions.Y, defaultResolver ], MONTH: [definitions.b, definitions.B, definitions.m, defaultResolver ], DAY: [definitions.a, definitions.A, definitions.e, definitions.d, defaultResolver ], HOUR: [definitions.H, definitions.l, definitions.p, definitions.P, function (hourFormat24, hourFormat12, ampmLower, ampmUpper) { let targetParam; let amOrpm; let isPM; let val; if (hourFormat12 && (amOrpm = (ampmLower || ampmUpper))) { if (amOrpm[0].parser(amOrpm[1]) === 'pm') { isPM = true; } targetParam = hourFormat12; } else if (hourFormat12) { targetParam = hourFormat12; } else { targetParam = hourFormat24; } if (!targetParam) { return null; } val = targetParam[0].parser(targetParam[1]); if (isPM) { val += 12; } return val; } ], MINUTE: [definitions.M, defaultResolver ], SECOND: [definitions.S, defaultResolver ] }; }; /* * Finds token from the format rule specified by a user. * @param format {String} : The format of the input date specified by the user * @return {Array} : An array of objects which contains the available token and their occurence index in the format */ DateTimeFormatter.findTokens = function (format) { const tokenPrefix = DateTimeFormatter.TOKEN_PREFIX; const definitions = DateTimeFormatter.getTokenDefinitions(); const tokenLiterals = Object.keys(definitions); const occurrence = []; let i; let forwardChar; while ((i = format.indexOf(tokenPrefix, i + 1)) >= 0) { forwardChar = format[i + 1]; if (tokenLiterals.indexOf(forwardChar) === -1) { continue; } occurrence.push({ index: i, token: forwardChar }); } return occurrence; }; /* * Format any JS date to a specified date given by user. * * @param date {Number | Date} : The date object which is to be formatted * @param format {String} : The format using which the date will be formatted for display */ DateTimeFormatter.formatAs = function (date, format) { const nDate = convertToNativeDate(date); const occurrence = DateTimeFormatter.findTokens(format); const definitions = DateTimeFormatter.getTokenDefinitions(); let formattedStr = String(format); const tokenPrefix = DateTimeFormatter.TOKEN_PREFIX; let token; let formattedVal; let i; let l; for (i = 0, l = occurrence.length; i < l; i++) { token = occurrence[i].token; formattedVal = definitions[token].formatter(nDate); formattedStr = formattedStr.replace(new RegExp(tokenPrefix + token, 'g'), formattedVal); } return formattedStr; }; /* * Parses the user specified date string to extract the date time params. * * @return {Array} : Value of date time params in an array [year, month, day, hour, minutes, seconds, milli] */ DateTimeFormatter.prototype.parse = function (dateTimeStamp, options) { const tokenResolver = DateTimeFormatter.tokenResolver(); const dtParams = this.extractTokenValue(dateTimeStamp); const dtParamSeq = DateTimeFormatter.DATETIME_PARAM_SEQUENCE; const noBreak = options && options.noBreak; const dtParamArr = []; const args = []; let resolverKey; let resolverParams; let resolverFn; let val; let i; let param; let resolvedVal; let l; let result = []; for (resolverKey in tokenResolver) { if (!{}.hasOwnProperty.call(tokenResolver, resolverKey)) { continue; } args.length = 0; resolverParams = tokenResolver[resolverKey]; resolverFn = resolverParams.splice(resolverParams.length - 1, 1)[0]; for (i = 0, l = resolverParams.length; i < l; i++) { param = resolverParams[i]; val = dtParams[param.name]; if (val === undefined) { args.push(null); } else { args.push([param, val]); } } resolvedVal = resolverFn.apply(this, args); if ((resolvedVal === undefined || resolvedVal === null) && !noBreak) { break; } dtParamArr[dtParamSeq[resolverKey]] = resolvedVal; } if (dtParamArr.length && this.checkIfOnlyYear(dtParamArr.length)) { result.unshift(dtParamArr[0], 0, 1); } else { result.unshift(...dtParamArr); } return result; }; /* * Extract the value of the token from user specified date time string. * * @return {Object} : An key value pair which contains the tokens as key and value as pair */ DateTimeFormatter.prototype.extractTokenValue = function (dateTimeStamp) { const format = this.format; const definitions = DateTimeFormatter.getTokenDefinitions(); const tokenPrefix = DateTimeFormatter.TOKEN_PREFIX; const occurrence = DateTimeFormatter.findTokens(format); const tokenObj = {}; let lastOccurrenceIndex; let occObj; let occIndex; let targetText; let regexFormat; let l; let i; regexFormat = String(format); const tokenArr = occurrence.map(obj => obj.token); const occurrenceLength = occurrence.length; for (i = occurrenceLength - 1; i >= 0; i--) { occIndex = occurrence[i].index; if (occIndex + 1 === regexFormat.length - 1) { lastOccurrenceIndex = occIndex; continue; } if (lastOccurrenceIndex === undefined) { lastOccurrenceIndex = regexFormat.length; } targetText = regexFormat.substring(occIndex + 2, lastOccurrenceIndex); regexFormat = regexFormat.substring(0, occIndex + 2) + RegExp.escape(targetText) + regexFormat.substring(lastOccurrenceIndex, regexFormat.length); lastOccurrenceIndex = occIndex; } for (i = 0; i < occurrenceLength; i++) { occObj = occurrence[i]; regexFormat = regexFormat.replace(tokenPrefix + occObj.token, definitions[occObj.token].extract()); } const extractValues = dateTimeStamp.match(new RegExp(regexFormat)) || []; extractValues.shift(); for (i = 0, l = tokenArr.length; i < l; i++) { tokenObj[tokenArr[i]] = extractValues[i]; } return tokenObj; }; /* * Give back the JS native date formed from user specified date string * * @return {Date} : Native JS Date */ DateTimeFormatter.prototype.getNativeDate = function (dateTimeStamp) { let date = null; if (Number.isFinite(dateTimeStamp)) { date = new Date(dateTimeStamp); } else if (!this.format && Date.parse(dateTimeStamp)) { date = new Date(dateTimeStamp); } else { const dtParams = this.dtParams = this.parse(dateTimeStamp); if (dtParams.length) { this.nativeDate = new Date(...dtParams); date = this.nativeDate; } } return date; }; DateTimeFormatter.prototype.checkIfOnlyYear = function(len) { return len === 1 && this.format.match(/y|Y/g).length; }; /* * Represents JS native date to a user specified format. * * @param format {String} : The format according to which the date is to be represented * @return {String} : The formatted date string */ DateTimeFormatter.prototype.formatAs = function (format, dateTimeStamp) { let nativeDate; if (dateTimeStamp) { nativeDate = this.nativeDate = this.getNativeDate(dateTimeStamp); } else if (!(nativeDate = this.nativeDate)) { nativeDate = this.getNativeDate(dateTimeStamp); } return DateTimeFormatter.formatAs(nativeDate, format); }; export { DateTimeFormatter as default };