highcharts
Version:
JavaScript charting framework
361 lines (360 loc) • 11.7 kB
JavaScript
/* *
*
* (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
*
* */
;
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;