/**
* Common utility methods
* @module util
* @since v0.1.1
*/
/**
* Convert date object to string formatted as 'M/d/yyyy HH:mm'
* Returns empty string if input is not a valid date
*
* @param {Date} d date object to convert
* @returns {String}
*/
export function displayDate(d) {
try {
const mm = `0${ d.getMinutes()}`.slice(-2),
hh = `0${ d.getHours()}`.slice(-2),
mo = d.getMonth() + 1,
dd = d.getDate(),
yyyy = d.getFullYear();
return `${mo}/${dd}/${yyyy} ${hh}:${mm}`;
} catch (err) {
return '';
}
}
/**
* Convert a date object to string formatted as 'HH:mm'
* Returns empty string if input is not a valid date
* @since v1.1.1
* @param {Date} d date object to convert
* @returns {String}
*/
export function displayTime(d) {
try {
const mm = `0${ d.getMinutes()}`.slice(-2);
const hh = `0${ d.getHours()}`.slice(-2);
return `${hh}:${mm}`;
} catch (err) {
return '';
}
}
/**
* Convert date and time raw input to a date object.
*
* @param {String} d date string from input field
* @param {String} t time string from input field
* @returns {Date} date object (returns 0 if input is invalid)
*/
export function getDateTime(d, t) {
t = checkTimeInput(t);
if ( d === "" || t === "" ) return 0;
const dy = +d.slice(0, 4),
dm = +d.slice(5, 7),
dd = +d.slice(8, 10),
th = +t.slice(0, 2),
tm = +t.slice(2, 4);
return new Date(dy, dm - 1, dd, th, tm, 0, 0);
}
/**
* Calculate the number of hours between two dates
*
* @param {Date} first first Date object
* @param {Date} second second Date object
* @returns {Number} hours between first and second date
*/
export function getHoursBetweenDates(first, second) {
return (second - first) / 1000 / 60 / 60;
}
/**
* Add hours to a date
*
* @param {Date} d Original date
* @param {Number} h Hours to add
* @returns {Date}
*/
export function addHoursToDate(d, h) {
const c = new Date(d);
c.setTime( c.getTime() + h * 60 * 60 * 1000 );
return c;
}
/**
* Rounds a number to a specified factor.
*
* @param {Number} x The number to round
* @param {Number} n The rounding factor
* @returns {Number} The rounded number
*/
export function roundTo(x, n = 0) {
if ( n <= 0 || isNaN(x) ) return x;
let t = Math.round(x / n) * n;
t = Math.floor(t * 1000);
t = t / 1000;
return t;
}
/**
* Displays a number, rounded, with units in the specified HTML element.
* If number is zero, clears input element instead, unless allowZero is true (then undefined will clear input)
* To get the result without specifying an HTML element, pass an empty string ('') for the first parameter.
*
* @param {String|HTMLElement} el Valid jQuery selector for target element.
* @param {Number|Number[]} [num = 0] The number or range to go in the input field. Range must be array with length of 2.
* @param {Number} [round = -1] The desired rounding factor
* @param {String} [unit = ""] Units to append to rounded value
* @param {String} [pre = ""] Text to prepend to rounded value
* @param {Boolean} [allowNegative = false] Accept negative values as valid
* @param {Boolean} [allowZero = false] Accept 0 as valid
* @returns {HTMLElement|String} The original DOM element for chaining, the function result if an empty string was provided instead of a selector, or an empty string if no HTMLElement specified
*/
export function displayValue( el, num = 0, round = -1, unit = "", pre = "", allowNegative = false, allowZero = false) {
// console.log(`[DISPLAY VALUE] ${el}, ${num}, ${round}, ${unit}, ${pre}, ${allowNegative}, ${allowZero}`);
let txt = '';
let wasNeg = false;
let wasNeg2 = false;
if ( Array.isArray(num) ) {
if ( num.length === 2 ) {
let [num1, num2] = num;
let num1Rounded, num2Rounded;
// Make numbers positive if they're negative and negative is allowed
if ( num1 < 0 && allowNegative ) {
num1 = 0 - num1;
wasNeg = true;
}
if ( num2 < 0 && allowNegative ) {
num2 = 0 - num2;
wasNeg2 = true;
}
// Round numbers if not zero, otherwise set rounded number to zero if zero is allowed
if ( num1 > 0 ) {
num1Rounded = roundTo(num1, round);
} else if ( num1 === 0 && allowZero ) {
num1Rounded = 0;
}
if ( num2 > 0 ) {
num2Rounded = roundTo(num2, round);
} else if ( num2 === 0 && allowZero ) {
num2Rounded = 0;
}
// Change back to negative if originally negative
if ( wasNeg ) {
num1Rounded = 0 - num1Rounded;
}
if ( wasNeg2 ) {
num2Rounded = 0 - num2Rounded;
}
// Express rounded numbers as a range, or a single number if they're equal
if ( num1 === num2 ) {
txt = num1;
} else {
txt = `${num1Rounded} - ${num2Rounded}`;
}
}
} else {
if ( num !== Infinity && num !== -Infinity ) {
// Make number positive if it's negative and negative is allowed
if ( num < 0 && allowNegative ) {
num = 0 - num;
wasNeg = true;
}
// Round number if not zero, otherwise set rounded number to zero if zero is allowed
if ( num > 0 ) {
txt = roundTo(num, round);
} else if ( num === 0 && allowZero ) {
txt = 0;
}
// Change back to negative if originally negative
if ( wasNeg ) {
txt = 0 - txt;
}
}
}
// Add pre and unit if input was valid
if ( txt !== '' ) {
txt = pre + txt + unit;
}
// return text if no target element was specified
if ( el === '' ) return txt;
// Set text of specified element to result
if ( $(el)[0].nodeName === "INPUT" ) {
$(el).val(txt);
} else {
$(el).html(txt);
}
return el;
}
/**
* Parses age input in years, months, days, or months+days and returns in years.
*
* @param {String} x Age input
* @returns {Number} Age in years (or undefined if invalid input)
*/
export function parseAge(x) {
// const yearsOld = 0;
if ( /^\d+ *[Dd]$/.test(x) ) {
const days = +x.replace(/ *d */gi, '');
return days / 365.25;
}
if ( /^\d+ *[Mm]$/.test(x) ) {
const months = +x.replace(/ *m */gi, '');
return months / 12;
}
if ( /^\d+ *[Mm]\d+ *[Dd]$/.test(x) ) {
const arrAge = x.split('m');
arrAge[1] = arrAge[1].replace('d', '');
return arrAge[0] / 12 + arrAge[1] / 365.25;
}
if ( isNaN(+x) ) return undefined;
return +x;
}
/**
* Evaluates a number, returns if is valid, between optional minimum
* and maximum, otherwise returns zero (or undefined if zero is allowed).
*
* @param {(Number|String)} x Value to check
* @param {Number} [min=-Infinity] Minimum of acceptable range
* @param {Number} [max=Infinity] Maximum of acceptable range
* @param {Boolean} [zeroAllowed=false] Is zero acceptable?
* @returns {Number} The input value if acceptable,
* zero if unacceptable, or undefined
* if unacceptable and zero is allowed
*/
export function checkValue(x, min = -Infinity, max = Infinity, zeroAllowed = false ) {
if ( zeroAllowed ) {
if ( x === "" ) return undefined;
}
x = parseFloat(x);
if ( typeof x === 'string' || isNaN(x) || x < min || x > max ) return 0;
return x;
}
/**
* Determines if a string of 1-4 numbers is a valid time (24-hr time, without a colon
* as 'H', 'HH', 'HMM', or 'HHMM'). If input is a valid time, returns the input as a
* 4-digit string as 'HHMM'. If not valid, returns an empty string.
*
* @param {Number|String} x - One to four digits representing a time
* @returns {String}
*/
export function checkTimeInput(x) {
/* Make sure x is a string */
x += "";
/* Just the hour: 0-9, 00-09, 10-19, 20-23 */
if (/(^[0-1]{0,1}[0-9]{1}$)|(^2[0-3]{1}$)/.test(x)) {
return `0${ x }00`.slice(-4);
}
/* Hour and minutes */
if ( /^(([0-1]{0,1}[0-9]{1})|2{1}[0-4]{1})[0-5]{1}[0-9]{1}$/.test(x) ) {
return `0${ x.slice(0, -2)}` .slice(-2) + x.slice(-2);
}
return "";
}
/**
* Convert RGB value to Hex value.
* @example
* rgbToHex('RGB(0, 0, 0)')
* rgbToHex('rgb(0, 0, 0)')
* rgbToHex('0, 0, 0')
* rgbToHex('0,0,0') *
*
* @param {String} val RGB value to convert
* @returns {String} Hex color value
*/
export function rgbToHex(val) {
const [r, g, b] = val instanceof Array ? val : val.replace(/[RrGgBb() ]/g, '').split(',');
if ( isNaN(r) || isNaN(g) || isNaN(b) || r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 ) return;
return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
}
/**
* Convert a hex color string to rgb values
*
* @param {String} h Hex color string, as #000000
* @returns {Number[]} Array of length 3, containing R, G, and B values
*/
export function hexToRgb(h) {
if ( /^(#?[a-fA-F0-9]{3})$/.test(h) ) {
h = `#${h[1]}${h[1]}${h[2]}${h[2]}${h[3]}${h[3]}`;
}
if ( /^([a-fA-F0-9]{6})$/.test(h) ) {
h = `#${h}`;
}
if ( !/^(#[a-fA-F0-9]{6})$/.test(h) ) {
console.warn(`hexToRgb : ${h} is not a valid hex color string`);
return;
}
if ( !/^(#[a-fA-F0-9]{6})$/.test(h) ) {
console.warn(`hexToRgb : ${h} is not a valid hex color string`);
return;
}
const r = `0x${ h[1] }${h[2]}`;
const g = `0x${ h[3] }${h[4]}`;
const b = `0x${ h[5] }${h[6]}`;
return [+r, +g, +b];
}
// function isDark(rgb) {
// const [r, g, b] = rgb;
// const hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b));
// if (hsp > 127.5) return false;
// return true;
// }
/**
* Provides a color, scaled between specified or default colors.
* Values in between color stops are scaled using R, G, and B values
* @param {Number} val Value to calculate color for
* @param {ColorStop[]} colors Stops and color values
* @returns {String} hex color code
*/
export function colorScale(val, colors) {
const max = colors.length - 1;
colors.forEach(c => {
const [r, g, b] = hexToRgb(c.hex);
c.red = r;
c.green = g;
c.blue = b;
});
if ( val > colors[max].stop ) return colors[max].hex;
if ( val <= colors[0].stop ) return colors[0].hex;
for ( let i = 1; i < max; i++ ) {
const me = colors[i];
if (val <= me.stop) {
const prev = colors[i - 1];
const scale = ( val - prev.stop ) / ( me.stop - prev.stop );
return rgbToHex([
Math.floor( prev.red + ( me.red - prev.red ) * scale ),
Math.floor( prev.green + ( me.green - prev.green ) * scale ),
Math.floor( prev.blue + ( me.blue - prev.blue ) * scale ),
]);
}
if ( i + 1 === max ) {
const next = colors[i + 1];
const scale = ( val - me.stop ) / ( next.stop - me.stop );
return rgbToHex([
Math.ceil( me.red - ( next.red - me.red ) * scale ),
Math.ceil( me.green - ( next.green - me.green ) * scale ),
Math.ceil( me.blue - ( next.blue - me.blue ) * scale ),
]);
}
}
}
/**
* Color Scale Options
*
* @typedef {Object} ColorStop
* @property {Number} stop - Value to set the color stop
* @property {String} hex - Hexadecimal color string
*/