UNPKG

highcharts

Version:
361 lines (360 loc) 11.7 kB
/* * * * (c) 2009-2026 Highsoft AS * * A commercial license may be required depending on use. * See www.highcharts.com/license * * * Authors: * - Sophie Bremer * - Sebastian Bochan * - Gøran Slettemark * - Torstein Hønsi * - Wojciech Chmiel * - Jomar Hønsi * - Kamil Kubik * * */ 'use strict'; import DataConverterUtils from './DataConverterUtils.js'; import { addEvent, fireEvent, merge } from '../../Shared/Utilities.js'; /* * * * Class * * */ /** * Base class providing an interface and basic methods for a DataConverter * * @private */ class DataConverter { /** * Adds a converter class to the registry. * * @private * * @param {string} key * Registry key of the converter class. * * @param {DataConverterTypes} DataConverterClass * Connector class (aka class constructor) to register. * * @return {boolean} * Returns true, if the registration was successful. False is returned, if * their is already a converter registered with this key. */ static registerType(key, DataConverterClass) { return (!!key && !DataConverter.types[key] && !!(DataConverter.types[key] = DataConverterClass)); } /* * * * Constructor * * */ /** * Constructs an instance of the DataConverter. * * @param {UserOptions} [options] * Options for the DataConverter. */ constructor(options) { /* * * * Properties * * */ /** * A collection of available date formats. */ this.dateFormats = { 'YYYY/mm/dd': { regex: /^(\d{4})([\-\.\/])(\d{1,2})\2(\d{1,2})$/, parser: function (match) { return (match ? Date.UTC(+match[1], +match[3] - 1, +match[4]) : NaN); } }, 'dd/mm/YYYY': { regex: /^(\d{1,2})([\-\.\/])(\d{1,2})\2(\d{4})$/, parser: function (match) { return (match ? Date.UTC(+match[4], +match[3] - 1, +match[1]) : NaN); }, alternative: 'mm/dd/YYYY' // Different format with the same regex }, 'mm/dd/YYYY': { regex: /^(\d{1,2})([\-\.\/])(\d{1,2})\2(\d{4})$/, parser: function (match) { return (match ? Date.UTC(+match[4], +match[1] - 1, +match[3]) : NaN); } }, 'dd/mm/YY': { regex: /^(\d{1,2})([\-\.\/])(\d{1,2})\2(\d{2})$/, parser: function (match) { const d = new Date(); if (!match) { return NaN; } let year = +match[4]; if (year > (d.getFullYear() - 2000)) { year += 1900; } else { year += 2000; } return Date.UTC(year, +match[3] - 1, +match[1]); }, alternative: 'mm/dd/YY' // Different format with the same regex }, 'mm/dd/YY': { regex: /^(\d{1,2})([\-\.\/])(\d{1,2})\2(\d{2})$/, parser: function (match) { return (match ? Date.UTC(+match[4] + 2000, +match[1] - 1, +match[3]) : NaN); } } }; const mergedOptions = merge(DataConverter.defaultOptions, options); let regExpPoint = mergedOptions.decimalPoint; if (regExpPoint === '.' || regExpPoint === ',') { regExpPoint = regExpPoint === '.' ? '\\.' : ','; this.decimalRegExp = new RegExp('^(-?[0-9]+)' + regExpPoint + '([0-9]+)$'); } this.options = mergedOptions; } /* * * * Functions * * */ /** * Converts a string value based on its guessed type. * * @param {*} value * The value to examine. * * @return {number | string | Date} * The converted value. */ convertByType(value) { const converter = this, typeMap = { 'number': (value) => DataConverterUtils.asNumber(value, converter.decimalRegExp), 'Date': (value) => DataConverterUtils.asDate(value, converter), 'string': DataConverterUtils.asString }; return typeMap[DataConverterUtils.guessType(value, converter)] .call(converter, value); } /** * Tries to guess the date format * - Check if either month candidate exceeds 12 * - Check if year is missing (use current year) * - Check if a shortened year format is used (e.g. 1/1/99) * - If no guess can be made, the user must be prompted * data is the data to deduce a format based on * @private * * @param {string[]} data * Data to check the format. * * @param {number} limit * Max data to check the format. * * @param {boolean} save * Whether to save the date format in the converter options. */ deduceDateFormat(data, limit, save) { const parser = this, stable = [], max = []; let format = 'YYYY/mm/dd', thing, guessedFormat = [], i = 0, madeDeduction = false, elem, j; if (!limit || limit > data.length) { limit = data.length; } for (; i < limit; i++) { if (typeof data[i] !== 'undefined' && data[i] && data[i].length) { thing = data[i] .trim() .replace(/[\-\.\/]/g, ' ') .split(' '); guessedFormat = [ '', '', '' ]; for (j = 0; j < thing.length; j++) { if (j < guessedFormat.length) { elem = parseInt(thing[j], 10); if (elem) { max[j] = (!max[j] || max[j] < elem) ? elem : max[j]; if (typeof stable[j] !== 'undefined') { if (stable[j] !== elem) { stable[j] = false; } } else { stable[j] = elem; } if (elem > 31) { if (elem < 100) { guessedFormat[j] = 'YY'; } else { guessedFormat[j] = 'YYYY'; } } else if (elem > 12 && elem <= 31) { guessedFormat[j] = 'dd'; madeDeduction = true; } else if (!guessedFormat[j].length) { guessedFormat[j] = 'mm'; } } } } } } if (madeDeduction) { // This handles a few edge cases with hard to guess dates for (j = 0; j < stable.length; j++) { if (stable[j] !== false) { if (max[j] > 12 && guessedFormat[j] !== 'YY' && guessedFormat[j] !== 'YYYY') { guessedFormat[j] = 'YY'; } } else if (max[j] > 12 && guessedFormat[j] === 'mm') { guessedFormat[j] = 'dd'; } } // If the middle one is dd, and the last one is dd, // the last should likely be year. if (guessedFormat.length === 3 && guessedFormat[1] === 'dd' && guessedFormat[2] === 'dd') { guessedFormat[2] = 'YY'; } format = guessedFormat.join('/'); // If the calculated format is not valid, we need to present an // error. } // Save the deduced format in the converter options. if (save) { parser.options.dateFormat = format; } return format; } /** * Emits an event on the DataConverter instance. * * @param {Event} [e] * Event object containing additional event data */ emit(e) { fireEvent(this, e.type, e); } /** * Registers a callback for a specific event. * * @param {string} type * Event type as a string. * * @param {DataEventCallback} callback * Function to register for an modifier callback. * * @return {Function} * Function to unregister callback from the modifier event. */ on(type, callback) { return addEvent(this, type, callback); } /** * Parse a date and return it as a number. * * @param {string} value * Value to parse. * * @param {string} dateFormatProp * Which of the predefined date formats * to use to parse date values. */ parseDate(value, dateFormatProp) { const converter = this, options = converter.options; let dateFormat = dateFormatProp || options.dateFormat, result = NaN, key, match = null; if (options.parseDate) { result = options.parseDate(value); } else { const dateFormats = converter.dateFormats; // Auto-detect the date format the first time if (!dateFormat) { for (key in dateFormats) { // eslint-disable-line guard-for-in const format = dateFormats[key]; match = value.match(format.regex); if (match) { dateFormat = key; result = format.parser(match); break; } } // Next time, use the one previously found } else { let format = dateFormats[dateFormat]; if (!format) { // The selected format is invalid format = dateFormats['YYYY/mm/dd']; } match = value.match(format.regex); if (match) { result = format.parser(match); } } // Fall back to Date.parse if (!match) { const parsed = Date.parse(value); if (!isNaN(parsed)) { result = parsed - new Date(parsed).getTimezoneOffset() * 60000; // Reset dates without year in Chrome if (!value.includes('2001') && new Date(result).getFullYear() === 2001) { result = NaN; } } } } return result; } } /* * * * Static Properties * * */ /** * Default options */ DataConverter.defaultOptions = { dateFormat: '', firstRowAsNames: true }; /** * Registry as a record object with converter names and their class. */ DataConverter.types = {}; /* * * * Default Export * * */ export default DataConverter;