489 lines
16 KiB
JavaScript
489 lines
16 KiB
JavaScript
/* *
|
|
*
|
|
* (c) 2012-2021 Highsoft AS
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* Authors:
|
|
* - Sophie Bremer
|
|
* - Gøran Slettemark
|
|
* - Torstein Hønsi
|
|
* - Wojciech Chmiel
|
|
*
|
|
* */
|
|
'use strict';
|
|
import U from './../Core/Utilities.js';
|
|
var merge = U.merge, isNumber = U.isNumber;
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
/**
|
|
* Class to convert between common value types.
|
|
*/
|
|
var DataConverter = /** @class */ (function () {
|
|
/* *
|
|
*
|
|
* Constructor
|
|
*
|
|
* */
|
|
/**
|
|
* Constructs an instance of the Data Converter.
|
|
*
|
|
* @param {DataConverter.Options} [options]
|
|
* Options for the Data Converter.
|
|
*
|
|
* @param {DataConverter.ParseDateCallbackFunction} [parseDate]
|
|
* A function to parse string representations of dates
|
|
* into JavaScript timestamps.
|
|
*/
|
|
function DataConverter(options, parseDate) {
|
|
/**
|
|
* A collection of available date formats.
|
|
*
|
|
* @name Highcharts.Data#dateFormats
|
|
* @type {Highcharts.Dictionary<Highcharts.DataDateFormatObject>}
|
|
*/
|
|
this.dateFormats = {
|
|
'YYYY/mm/dd': {
|
|
regex: /^([0-9]{4})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{1,2})$/,
|
|
parser: function (match) {
|
|
return (match ?
|
|
Date.UTC(+match[1], match[2] - 1, +match[3]) :
|
|
NaN);
|
|
}
|
|
},
|
|
'dd/mm/YYYY': {
|
|
regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/,
|
|
parser: function (match) {
|
|
return (match ?
|
|
Date.UTC(+match[3], match[2] - 1, +match[1]) :
|
|
NaN);
|
|
},
|
|
alternative: 'mm/dd/YYYY' // different format with the same regex
|
|
},
|
|
'mm/dd/YYYY': {
|
|
regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/,
|
|
parser: function (match) {
|
|
return (match ?
|
|
Date.UTC(+match[3], match[1] - 1, +match[2]) :
|
|
NaN);
|
|
}
|
|
},
|
|
'dd/mm/YY': {
|
|
regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/,
|
|
parser: function (match) {
|
|
var d = new Date();
|
|
if (!match) {
|
|
return NaN;
|
|
}
|
|
var year = +match[3];
|
|
if (year > (d.getFullYear() - 2000)) {
|
|
year += 1900;
|
|
}
|
|
else {
|
|
year += 2000;
|
|
}
|
|
return Date.UTC(year, match[2] - 1, +match[1]);
|
|
},
|
|
alternative: 'mm/dd/YY' // different format with the same regex
|
|
},
|
|
'mm/dd/YY': {
|
|
regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/,
|
|
parser: function (match) {
|
|
return (match ?
|
|
Date.UTC(+match[3] + 2000, match[1] - 1, +match[2]) :
|
|
NaN);
|
|
}
|
|
}
|
|
};
|
|
var decimalPoint;
|
|
this.options = merge(DataConverter.defaultOptions, options);
|
|
this.parseDateFn = parseDate;
|
|
decimalPoint = this.options.decimalPoint;
|
|
if (decimalPoint !== '.' && decimalPoint !== ',') {
|
|
decimalPoint = void 0;
|
|
}
|
|
this.decimalRegex = (decimalPoint &&
|
|
new RegExp('^(-?[0-9]+)' + decimalPoint + '([0-9]+)$'));
|
|
}
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
/**
|
|
* Getter for a date format.
|
|
*/
|
|
DataConverter.prototype.getDateFormat = function () {
|
|
return this.options.dateFormat;
|
|
};
|
|
/**
|
|
* Converts a value to a boolean.
|
|
*
|
|
* @param {DataConverter.Type} value
|
|
* Value to convert.
|
|
*
|
|
* @return {boolean}
|
|
* Converted value as a boolean.
|
|
*/
|
|
DataConverter.prototype.asBoolean = function (value) {
|
|
if (typeof value === 'boolean') {
|
|
return value;
|
|
}
|
|
if (typeof value === 'string') {
|
|
return value !== '' && value !== '0' && value !== 'false';
|
|
}
|
|
return !!this.asNumber(value);
|
|
};
|
|
/**
|
|
* Converts a value to a Date.
|
|
*
|
|
* @param {DataConverter.Type} value
|
|
* Value to convert.
|
|
*
|
|
* @return {globalThis.Date}
|
|
* Converted value as a Date.
|
|
*/
|
|
DataConverter.prototype.asDate = function (value) {
|
|
var timestamp;
|
|
if (typeof value === 'string') {
|
|
timestamp = this.parseDate(value);
|
|
}
|
|
else if (typeof value === 'number') {
|
|
timestamp = value;
|
|
}
|
|
else if (value instanceof Date) {
|
|
return value;
|
|
}
|
|
else {
|
|
timestamp = this.parseDate(this.asString(value));
|
|
}
|
|
return new Date(timestamp);
|
|
};
|
|
/**
|
|
* Converts a value to a number.
|
|
*
|
|
* @param {DataConverter.Type} value
|
|
* Value to convert.
|
|
*
|
|
* @return {number}
|
|
* Converted value as a number.
|
|
*/
|
|
DataConverter.prototype.asNumber = function (value) {
|
|
if (typeof value === 'number') {
|
|
return value;
|
|
}
|
|
if (typeof value === 'boolean') {
|
|
return value ? 1 : 0;
|
|
}
|
|
if (typeof value === 'string') {
|
|
if (value.indexOf(' ') > -1) {
|
|
value = value.replace(/\s+/g, '');
|
|
}
|
|
if (this.decimalRegex) {
|
|
value = value.replace(this.decimalRegex, '$1.$2');
|
|
}
|
|
return parseFloat(value);
|
|
}
|
|
if (value instanceof Date) {
|
|
return value.getDate();
|
|
}
|
|
if (value) {
|
|
return value.getRowCount();
|
|
}
|
|
return NaN;
|
|
};
|
|
/**
|
|
* Converts a value to a string.
|
|
*
|
|
* @param {DataConverter.Type} value
|
|
* Value to convert.
|
|
*
|
|
* @return {string}
|
|
* Converted value as a string.
|
|
*/
|
|
DataConverter.prototype.asString = function (value) {
|
|
return "".concat(value);
|
|
};
|
|
/**
|
|
* Trim a string from whitespaces.
|
|
*
|
|
* @param {string} str
|
|
* String to trim.
|
|
*
|
|
* @param {boolean} [inside=false]
|
|
* Remove all spaces between numbers.
|
|
*
|
|
* @return {string}
|
|
* Trimed string
|
|
*/
|
|
DataConverter.prototype.trim = function (str, inside) {
|
|
var converter = this;
|
|
if (typeof str === 'string') {
|
|
str = str.replace(/^\s+|\s+$/g, '');
|
|
// Clear white space insdie the string, like thousands separators
|
|
if (inside && /^[0-9\s]+$/.test(str)) {
|
|
str = str.replace(/\s/g, '');
|
|
}
|
|
if (converter.decimalRegex) {
|
|
str = str.replace(converter.decimalRegex, '$1.$2');
|
|
}
|
|
}
|
|
return str;
|
|
};
|
|
/**
|
|
* Guesses the potential type of a string value
|
|
* (for parsing CSV etc)
|
|
*
|
|
* @param {string} value
|
|
* The string to examine
|
|
* @return {'number'|'string'|'Date'}
|
|
* `string`, `Date` or `number`
|
|
*/
|
|
DataConverter.prototype.guessType = function (value) {
|
|
var converter = this, trimVal = converter.trim(value), trimInsideVal = converter.trim(value, true), floatVal = parseFloat(trimInsideVal);
|
|
var result = 'string', dateVal;
|
|
// is numeric
|
|
if (+trimInsideVal === floatVal) {
|
|
// If the number is greater than milliseconds in a year, assume
|
|
// datetime.
|
|
if (floatVal > 365 * 24 * 3600 * 1000) {
|
|
result = 'Date';
|
|
}
|
|
else {
|
|
result = 'number';
|
|
}
|
|
// String, continue to determine if it is
|
|
// a date string or really a string.
|
|
}
|
|
else {
|
|
if (trimVal && trimVal.length) {
|
|
dateVal = converter.parseDate(value);
|
|
}
|
|
if (dateVal && isNumber(dateVal)) {
|
|
result = 'Date';
|
|
}
|
|
else {
|
|
result = 'string';
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
/**
|
|
* Casts a string value to it's guessed type
|
|
* @param {string} value
|
|
* The string to examine
|
|
*
|
|
* @return {number|string|Date}
|
|
* The converted value
|
|
*/
|
|
DataConverter.prototype.asGuessedType = function (value) {
|
|
var converter = this, typeMap = {
|
|
'number': converter.asNumber,
|
|
'Date': converter.asDate,
|
|
'string': converter.asString
|
|
};
|
|
return typeMap[converter.guessType(value)].call(converter, value);
|
|
};
|
|
/**
|
|
* Parse a date and return it as a number.
|
|
*
|
|
* @function Highcharts.Data#parseDate
|
|
*
|
|
* @param {string} value
|
|
* Value to parse.
|
|
*
|
|
* @param {string} dateFormatProp
|
|
* Which of the predefined date formats
|
|
* to use to parse date values.
|
|
*/
|
|
DataConverter.prototype.parseDate = function (value, dateFormatProp) {
|
|
var converter = this;
|
|
var dateFormat = dateFormatProp || converter.options.dateFormat, result = NaN, key, format, match;
|
|
if (converter.parseDateFn) {
|
|
result = converter.parseDateFn(value);
|
|
}
|
|
else {
|
|
// Auto-detect the date format the first time
|
|
if (!dateFormat) {
|
|
for (key in converter.dateFormats) { // eslint-disable-line guard-for-in
|
|
format = converter.dateFormats[key];
|
|
match = value.match(format.regex);
|
|
if (match) {
|
|
// converter.options.dateFormat = dateFormat = key;
|
|
dateFormat = key;
|
|
// converter.options.alternativeFormat =
|
|
// format.alternative || '';
|
|
result = format.parser(match);
|
|
break;
|
|
}
|
|
}
|
|
// Next time, use the one previously found
|
|
}
|
|
else {
|
|
format = converter.dateFormats[dateFormat];
|
|
if (!format) {
|
|
// The selected format is invalid
|
|
format = converter.dateFormats['YYYY/mm/dd'];
|
|
}
|
|
match = value.match(format.regex);
|
|
if (match) {
|
|
result = format.parser(match);
|
|
}
|
|
}
|
|
// Fall back to Date.parse
|
|
if (!match) {
|
|
match = Date.parse(value);
|
|
// External tools like Date.js and MooTools extend Date object
|
|
// and returns a date.
|
|
if (typeof match === 'object' &&
|
|
match !== null &&
|
|
match.getTime) {
|
|
result = (match.getTime() -
|
|
match.getTimezoneOffset() *
|
|
60000);
|
|
// Timestamp
|
|
}
|
|
else if (isNumber(match)) {
|
|
result = match - (new Date(match)).getTimezoneOffset() * 60000;
|
|
if ( // reset dates without year in Chrome
|
|
value.indexOf('2001') === -1 &&
|
|
(new Date(result)).getFullYear() === 2001) {
|
|
result = NaN;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
/**
|
|
* 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 {Array<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.
|
|
*/
|
|
DataConverter.prototype.deduceDateFormat = function (data, limit, save) {
|
|
var parser = this, stable = [], max = [];
|
|
var format = 'YYYY/mm/dd', thing, guessedFormat = [], i = 0, madeDeduction = false,
|
|
// candidates = {},
|
|
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, ' ')
|
|
.replace(/\-/g, ' ')
|
|
.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';
|
|
}
|
|
// madeDeduction = true;
|
|
}
|
|
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 caculated 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;
|
|
};
|
|
/* *
|
|
*
|
|
* Static Properties
|
|
*
|
|
* */
|
|
/**
|
|
* Default options
|
|
*/
|
|
DataConverter.defaultOptions = {
|
|
dateFormat: '',
|
|
alternativeFormat: ''
|
|
};
|
|
return DataConverter;
|
|
}());
|
|
/* *
|
|
*
|
|
* Export
|
|
*
|
|
* */
|
|
export default DataConverter;
|