899 lines
26 KiB
JavaScript
899 lines
26 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
|
|
const exec = require('child_process').exec,
|
|
debug = require('debug')('agm:utils'),
|
|
moment = require('moment'),
|
|
ms = require('ms'),
|
|
path = require('path'),
|
|
fs = require('fs'),
|
|
URL = require('url').URL,
|
|
ObjectId = require('mongodb').ObjectId,
|
|
CVCST = require('./convert_constants'),
|
|
Bignumber = require('bignumber.js'),
|
|
RecTypes = require('./constants').RecTypes;
|
|
|
|
function waitUntil(asyncTest, timeout, interval) {
|
|
const endTime = Number(new Date()) + (timeout || 2000);
|
|
interval = interval || 100;
|
|
|
|
const checkCondition = (function (resolve, reject) {
|
|
asyncTest()
|
|
.then(function (value) {
|
|
if (value === true) {
|
|
resolve(true);
|
|
} else if (Number(new Date()) < endTime) {
|
|
setTimeout(checkCondition, interval, resolve, reject);
|
|
}
|
|
else {
|
|
reject(new Error('timed out for ' + asyncTest + ': ' + arguments));
|
|
}
|
|
})
|
|
.catch(() => reject())
|
|
});
|
|
|
|
return new Promise(checkCondition);
|
|
}
|
|
|
|
/**
|
|
* Delay Promise function
|
|
* @param {*} t time in milliseconds
|
|
* @param {*} v resolve function
|
|
*/
|
|
function delay(t, v) {
|
|
return new Promise(function (resolve) {
|
|
setTimeout(resolve.bind(null, v), t);
|
|
});
|
|
}
|
|
|
|
function execAsync(cmd) {
|
|
return new Promise(
|
|
function (resolve, reject) {
|
|
exec(cmd, (error, stdout /*,stderr*/) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(stdout);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function download(url, dest, cb) {
|
|
const wUrl = new URL(url);
|
|
const file = fs.createWriteStream(dest);
|
|
|
|
const requestor = require(wUrl.protocol.replace(':', ''));
|
|
requestor.get(url, function (response) {
|
|
response.pipe(file);
|
|
file.on('finish', function () {
|
|
file.close(cb); // close() is async, call cb after close completes.
|
|
});
|
|
}).on('error', function (err) { // Handle errors
|
|
fs.unlink(dest); // Delete the file async. (But we don't check the result)
|
|
if (cb) cb(err.message);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get the rate unit string by the rateUnit type
|
|
* @param {*} rateUnit the rateUnit type
|
|
* @param {*} inShort true: in short string or else long rate string
|
|
* @param {*} part 1: first part or second part
|
|
*/
|
|
function rateUnitString(rateUnit, isShort, part) {
|
|
let result = '';
|
|
|
|
switch (rateUnit) {
|
|
case 0:
|
|
result = isShort ? 'oz/ac' : 'ounces/acre';
|
|
break;
|
|
case 1:
|
|
result = isShort ? 'gal/ac' : 'gallons/acre';
|
|
break;
|
|
case 2:
|
|
result = isShort ? 'lbs/ac' : 'pounds/arce';
|
|
break;
|
|
case 3:
|
|
result = isShort ? 'lit/ha' : 'liters/hectare';
|
|
break;
|
|
case 4:
|
|
result = isShort ? 'kg/ha' : 'kilograms/hectare';
|
|
break;
|
|
}
|
|
if (part) {
|
|
result = part === 1 ? result.split('/')[0] : result.split('/')[1];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Infer to number Application Rate Unit from text
|
|
* Default: isUS == true ? 1 (gals/ac) : 3 (lit/ha)
|
|
* @param {*} rateStr rate string from APP RATE of a q file
|
|
* @param {*} isUS true: is US measurement
|
|
* */
|
|
function rateStringToCode(rateStr, isUS) {
|
|
if (rateStr && typeof rateStr === 'string') {
|
|
const str = rateStr.trim();
|
|
const strLC = str.toLowerCase();
|
|
if (strLC.startsWith("oz/ac") || str === "OZPA")
|
|
return 0;
|
|
if (strLC === "gals/acr" || strLC.startsWith("gallons/ac") || str === "GPA")
|
|
return 1;
|
|
if (strLC === "lbs/acr" || str === "LBPA")
|
|
return 2;
|
|
if (strLC === "lit/ha" || strLC.startsWith("liters/hectare") || str === "LPH")
|
|
return 3;
|
|
if (strLC === "kg/ha" || str === "KGPH")
|
|
return 4;
|
|
}
|
|
else {
|
|
return isUS ? 1 : 3;
|
|
}
|
|
}
|
|
|
|
function isUSRateUnit(rateUnit) {
|
|
return (rateUnit >= 0 && rateUnit <= 2);
|
|
}
|
|
|
|
function areaUnitString(isUS, isShort) {
|
|
if (isShort)
|
|
return isUS ? 'ac' : 'ha';
|
|
else
|
|
return isUS ? 'acres' : 'hectares';
|
|
}
|
|
|
|
/**
|
|
* Get the application info from file meta data (read from Qfile)
|
|
* @param {*} fileMeta meta data JSON object read from from QFile
|
|
* @param {*} recType initial record type. For AGNAV data: Binary (1: AGN_BIN_LQD), Shape: (4: AGN_SHP). Initially assumed as liquid type.
|
|
* @returns application rate info object { appRate: , rateUnit: , recType: }
|
|
*/
|
|
function rateInfoFromFileMeta(fileMeta, recType) {
|
|
const rateInfo = { appRate: -1, rateUnit: -1, recType: recType, useFC: true };
|
|
|
|
if (fileMeta.hasQfile) {
|
|
const _fcType = fileMeta && fileMeta.fcType && fileMeta.fcType.trim().toLowerCase();
|
|
|
|
if (_fcType && _fcType.length && !_fcType.match(/none/i)) {
|
|
if (_fcType.includes('gate') || _fcType.includes('granular') || _fcType.includes('release')) {
|
|
rateInfo.recType = recType === RecTypes.AGN_SHP ? RecTypes.AGN_SHP_DRY : RecTypes.AGN_BIN_DRY;
|
|
}
|
|
}
|
|
else {
|
|
rateInfo.useFC = false;
|
|
}
|
|
rateInfo.rateUnit = rateStringToCode(fileMeta.appRateUnitStr);
|
|
} else {
|
|
rateInfo.useFC = false;
|
|
rateInfo.rateUnit = fileMeta.rateUnit;
|
|
}
|
|
rateInfo.appRate = fileMeta.appRate;
|
|
|
|
return rateInfo;
|
|
}
|
|
|
|
/**
|
|
* Calculate applied application rate from flow rate, swath and speed
|
|
* @param flowRate flowrate in lit/min
|
|
* @param swath swath width in meters
|
|
* @param speed speed in m/s
|
|
* @returns applied rate in lit/ha
|
|
*/
|
|
function appRateFromFlowRate(flowRate, swath, speed) {
|
|
if (speed > 0 && swath > 0)
|
|
return (flowRate * CVCST.HA2SM) / (speed * swath * 60.0);
|
|
else
|
|
return 0.0;
|
|
}
|
|
|
|
function toMetricRate(value, rateUnit) {
|
|
const toMap = {
|
|
0: { value: value * 0.0730778, unit: 3 }, // oz(fluid)/ac => lit/ha
|
|
1: { value: value * 9.35396, unit: 3 }, // gal/ac => lit/ha
|
|
2: { value: value * 1.12085, unit: 4 } // lbs/ac => kg/ha
|
|
}
|
|
return toMap[rateUnit] ? toMap[rateUnit] : { value: value, unit: rateUnit };
|
|
}
|
|
|
|
/*!
|
|
* Convert degree [0..359] cycle to wind compass rose text
|
|
* @param {*} deg degree
|
|
* @return one of the text among the list {"N","NNE","NE","ENE","E","ESE", "SE", "SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"}
|
|
*/
|
|
function deg2Compass(deg) {
|
|
/* from: http://stackoverflow.com/questions/7490660/converting-wind-direction-in-angles-to-text-words
|
|
* Steps:
|
|
1. Divide the angle by 22.5 because 360deg/16 directions = 22.5deg/direction change.
|
|
2. Add .5 so that when you truncate the value you can break the 'tie' between the change threshold.
|
|
3. Truncate the value using integer division (so there is no rounding).
|
|
4. Directly index into the array and print the value (mod 16).
|
|
*/
|
|
|
|
const compass = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"];
|
|
const val = Math.trunc((deg / 22.5) + .5);
|
|
return compass[val % 16];
|
|
}
|
|
|
|
/**
|
|
* Format the Celcius degree to F String depends on the isUS. If NaN return ''
|
|
* @param {*} degC
|
|
* @param {*} isUS
|
|
* @param {*} withUnitString
|
|
*/
|
|
function inCorF(degC, isUS, withUnitString) {
|
|
if (!isNumber(degC)) return '';
|
|
|
|
if (!withUnitString)
|
|
return (isUS ? ((1.8 * degC) + 32) : degC).toFixed();
|
|
else {
|
|
const unitString = isUS ? "°F" : "°C";
|
|
return (isUS ? ((1.8 * degC) + 32) : degC).toFixed() + unitString;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append items from array 2 to array 1. If array1 is empty return array 2.
|
|
* @param {*} arr1
|
|
* @param {*} arr2
|
|
*/
|
|
function appendArray(arr1, arr2) {
|
|
if (isEmptyArray(arr2)) return arr1;
|
|
if (!Array.isArray(arr1))
|
|
return arr2;
|
|
return arr1.concat(arr2);
|
|
}
|
|
|
|
function isEmptyArray(theArray) {
|
|
return (!theArray || !Array.isArray(theArray) || !theArray.length);
|
|
}
|
|
|
|
function isEmptyStr(str) {
|
|
return (!str || !str.length);
|
|
}
|
|
|
|
/**
|
|
* Check whether an object is null or empty
|
|
* @param {*} obj the Object to test.
|
|
* @returns true or false on whether the object is null/undefined or empty object {}.
|
|
*/
|
|
function isEmptyObj(obj) {
|
|
if (!obj) return true;
|
|
for (const prop in obj) {
|
|
if (obj.hasOwnProperty(prop))
|
|
return false;
|
|
}
|
|
return JSON.stringify(obj) === JSON.stringify({});
|
|
}
|
|
|
|
function getField(theObject, value, defaultVal) {
|
|
if (!theObject) return defaultVal || '';
|
|
return theObject[value] ? theObject[value] : defaultVal || '';
|
|
}
|
|
|
|
function isBlank(str) {
|
|
return (!str || /^\s*$/.test(str));
|
|
}
|
|
|
|
function isObjectId(val) {
|
|
return val && ObjectId.isValid(val);
|
|
}
|
|
|
|
function stringToBoolean(string) {
|
|
if (typeof string === "boolean") return string;
|
|
if (typeof string === "number") return string > 0;
|
|
if (string === undefined || string === null) return false;
|
|
|
|
switch (string.toLowerCase().trim()) {
|
|
case "true": case "yes": case "y": case "1": return true;
|
|
case "false": case "no": case "n": case "0": case null: return false;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
function ensureString(str) {
|
|
return str ? str.toString() : '';
|
|
}
|
|
|
|
function noLinebreaks(str) {
|
|
return !str ? '' : ensureString(str).replace(/\r?\n|\r/g, '');
|
|
}
|
|
|
|
function noSpecialChars(str) {
|
|
return !str ? '' : ensureString(str).replace(/[^a-zA-Z0-9]/g, '_').replace(/_{2,}/g, '_').replace(/_$/, '');
|
|
}
|
|
|
|
function trimStrTo(str, maxLength) {
|
|
if (!maxLength) return str;
|
|
let _str = ensureString(str) || '';
|
|
if (!_str) return '';
|
|
|
|
_str = _str.trim();
|
|
const diff = _str.length - maxLength;
|
|
_str = diff > 0 ? _str.substring(0, _str.length - diff).trim() : _str;
|
|
return _str;
|
|
}
|
|
|
|
function removeEndChars(str, char) {
|
|
let _str = ensureString(str) || '';
|
|
if (!_str) return '';
|
|
if (!char) return str;
|
|
while (_str.endsWith(char)) {
|
|
_str = _str.substring(0, _str.length - 1);
|
|
}
|
|
return _str;
|
|
}
|
|
|
|
function normalizeName(str, maxLength) {
|
|
const _maxLength = maxLength ? maxLength : 20;
|
|
return removeEndChars(trimStrTo(noSpecialChars(str), _maxLength), "_")
|
|
}
|
|
|
|
/**
|
|
* Split a string by first pattern then values by 2nd pattern
|
|
* @param {*} str the line string value
|
|
* @param {*} first the first delimeters or regex string
|
|
* @param {*} second the second delimeters or regex string
|
|
*/
|
|
function split2nd(str, first = ':', second = /[\t|\s]+/) {
|
|
if (!str || !first)
|
|
return [str];
|
|
const _str = str.substring(str.indexOf(first) + 1).trim();
|
|
return second ? _str.split(second) : [_str];
|
|
}
|
|
|
|
function line2ndFields(line, splitVal = false, delimeter = ':') {
|
|
return splitVal ? split2nd(line, delimeter) : split2nd(line, delimeter, null);
|
|
}
|
|
|
|
/**
|
|
* Convert length value, feet, to meters if isUS is true
|
|
*/
|
|
function toMeter(value, isUS) {
|
|
return isUS ? value * 0.3048 : value;
|
|
}
|
|
|
|
function mpSecToKnot(mpsec) {
|
|
if (!isNumber(Number(mpsec))) return 0;
|
|
return Number(mpsec) * 1.94384;
|
|
}
|
|
|
|
function toArea(value, isUS = true, isValueHa = false) {
|
|
let result = 0.0;
|
|
if (!value) return result;
|
|
|
|
if (isValueHa)
|
|
result = isUS ? value * 2.47105 : value; // if US convert ha => acre
|
|
else
|
|
result = isUS ? value / 4046.86 : value / 10000; // square meters to ha or acre
|
|
return result;
|
|
}
|
|
|
|
function toVolume(value, isLiquid, isUS = true) {
|
|
if (!value) return 0.0;
|
|
if (isLiquid)
|
|
return isUS ? (value * 0.264172) : value; // If US convert liters to gallons
|
|
else
|
|
return isUS ? (value * 2.20462) : value; // If US convert kilograms to pounds
|
|
}
|
|
|
|
function toMetricVolume(value, isLiquid, isUS = true) {
|
|
if (!value) return 0.0;
|
|
if (isLiquid)
|
|
return isUS ? (value / 0.264172) : value; // If US convert gallons to litters
|
|
else
|
|
return isUS ? (value / 2.20462) : value; // If US convert pounds to kilograms
|
|
}
|
|
|
|
function acreToHa(acre) {
|
|
return acre ? acre / 2.471 : 0;
|
|
}
|
|
|
|
function haToAcre(ha) {
|
|
return ha ? ha * 2.471 : 0;
|
|
}
|
|
|
|
function ozToGal(ozs) {
|
|
return ozs * 0.0078125; // ozs to gals
|
|
}
|
|
|
|
function getFirstProp(obj, defaultVal) {
|
|
if (!obj || !Object.keys(obj).length) return defaultVal;
|
|
for (let prop in obj) {
|
|
if (obj[prop]);
|
|
return obj[prop] || defaultVal;
|
|
}
|
|
}
|
|
|
|
function hasProperty(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
function getProp(obj, prop, defaultVal) {
|
|
if (obj === undefined || prop === undefined) return null;
|
|
prop = (prop + "").toLowerCase();
|
|
for (const p in obj) {
|
|
if (hasProperty(obj, p) && prop == (p + "").toLowerCase()) {
|
|
return obj[p] || defaultVal;
|
|
}
|
|
}
|
|
return defaultVal || null;
|
|
}
|
|
|
|
function getPropNum(obj, prop, defaultVal) {
|
|
let _defaultVal = +defaultVal;
|
|
if (!isNumber(_defaultVal)) _defaultVal = 0;
|
|
|
|
let val = +getProp(obj, prop, _defaultVal);
|
|
return isNumber(val) ? val : _defaultVal;
|
|
}
|
|
|
|
function dateTimePartsFromAgNav(agn, format = 1) {
|
|
if (agn && /^\d{9}/i.test(agn)) {
|
|
let day = agn.substring(3, 5);
|
|
let month = agn.substring(1, 3);
|
|
let agnYear = agn.substring(0, 1);
|
|
const fullYear = (new Date()).getFullYear().toString();
|
|
|
|
let thirdNumYear = fullYear.substring(2, 3);
|
|
if (Number(thirdNumYear + agnYear) > Number(fullYear.substring(2)))
|
|
thirdNumYear = thirdNumYear - 1;
|
|
|
|
let year = `${fullYear.substring(0, 2)}${thirdNumYear}${agnYear}`;
|
|
let hour = agn.substring(5, 7);
|
|
let minute = agn.substring(7, 9);
|
|
|
|
let date, time;
|
|
switch (format) {
|
|
case 1:
|
|
date = `${year}-${month}-${day}`;
|
|
time = `${hour}:${minute}:00`;
|
|
break;
|
|
default:
|
|
date = `${year}${month}${day}`;
|
|
time = `${hour}${minute}00`;
|
|
break;
|
|
}
|
|
return { date: date, time: time };
|
|
}
|
|
else return null;
|
|
}
|
|
|
|
function toUTCDateTime(seconds, dateStr) {
|
|
return new moment.utc(`${dateStr}T${new Date(1000 * seconds).toISOString().substring(11, 19).replace(/:/g, '')}`);
|
|
}
|
|
|
|
function isValidDate(dateVal) {
|
|
if (!dateVal) return false;
|
|
if (dateVal instanceof Date) {
|
|
return isNumber(dateVal.valueOf());
|
|
}
|
|
else {
|
|
let date = new Date(dateVal);
|
|
return date instanceof Date && isNumber(date.valueOf());
|
|
}
|
|
}
|
|
|
|
function datafileNameToAgnString(filename) {
|
|
if (isEmptyStr(filename)) return null;
|
|
|
|
const baseName = path.basename(filename);
|
|
if (baseName.length < 10) return null;
|
|
|
|
if (/t/i.test(baseName))
|
|
return baseName.substring(1, 8) + path.extname(baseName).substring(2, 4);
|
|
else
|
|
return path.basename(baseName).substring(1, 10);
|
|
}
|
|
|
|
/**
|
|
* Get a locale specific formatted numeric text from a number
|
|
* @param {*} value the number
|
|
* @param {*} fixedTo number of decimal
|
|
* @param {*} lang language code in short i.e.: es, pt,..
|
|
* @note: Ref: https://github.com/unicode-org/full-icu-npm
|
|
*/
|
|
function toLocaleStr(value, fixedTo, lang = 'en') {
|
|
let cValue;
|
|
if (isNumber(value)) {
|
|
cValue = Number(value).toLocaleString(lang, {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: fixedTo
|
|
});
|
|
return cValue;
|
|
}
|
|
else return '';
|
|
}
|
|
|
|
// FILES
|
|
function ArrayBuffertoBuffer(ab) {
|
|
return Buffer.from(new Uint8Array(ab));
|
|
}
|
|
//
|
|
|
|
/**
|
|
* Dynamicall sort an array of object by a fieldname (case-sensitive)
|
|
* From: https://stackoverflow.com/questions/1129216/sort-array-of-objects-by-string-property-value-in-javascript
|
|
* @param {any} property
|
|
* @returns sorted condition for each call from array.sort(fn)
|
|
*/
|
|
function dynamicSort(property, isNumber = false) {
|
|
let sortOrder = 1;
|
|
if (property[0] === "-") {
|
|
sortOrder = -1;
|
|
property = property.substring(1);
|
|
}
|
|
return function (a, b) {
|
|
let result;
|
|
if (isNumber)
|
|
result = (Number(a[property]) < Number(b[property])) ? -1 : (Number(a[property]) > Number(b[property]));
|
|
else
|
|
result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
|
|
return result * sortOrder;
|
|
}
|
|
}
|
|
|
|
function bareFilename(filename, keepPath = false) {
|
|
if (!filename)
|
|
return filename;
|
|
return !keepPath ? path.basename(filename).replace(/\.[^/\\.]+$/, "") : filename.replace(/\.[^/\\.]+$/, "");
|
|
}
|
|
|
|
/**
|
|
* Print all objects properties's value. Just used while debugging
|
|
* @param {*} arr
|
|
* @param {*} prop
|
|
*/
|
|
function printByProp(arr, prop) {
|
|
if (!arr || !Array.isArray(arr)) return;
|
|
let items = [];
|
|
for (const item of arr) {
|
|
if (prop in item)
|
|
items.push(item[prop]);
|
|
}
|
|
console.log(items);
|
|
}
|
|
|
|
/**
|
|
* Returns an array with arrays of the given size.
|
|
*
|
|
* @param myArray {Array} Array to split
|
|
* @param chunkSize {Integer} Size of every group, default = 1000 items
|
|
*/
|
|
function chunkArray(myArray, chunk_size = 1000) {
|
|
let chunks = [],
|
|
i = 0,
|
|
n = myArray.length;
|
|
while (i < n) {
|
|
chunks.push(myArray.slice(i, i += chunk_size));
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
/**
|
|
* Check wether a value is exactly a number
|
|
* Ref: https://byby.dev/js-check-number
|
|
* @param {*} value
|
|
* @returns {boolean} true or false
|
|
*/
|
|
function isNumber(value) {
|
|
return typeof value === "number" && !Number.isNaN(value);
|
|
// return !isNaN(value) && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
/**
|
|
* Truncate decimal to number of decimal in precision. Truncate the numbers of decimal digits only thus does not do rounding backward.
|
|
* @param {*} value
|
|
* @param {*} digits
|
|
* @returns
|
|
*/
|
|
function fixedTo(value, digits) {
|
|
if (undefined === value) return 0;
|
|
let re = new RegExp("(-?\\d+\\.\\d{" + digits + "})(\\d)"),
|
|
m = value.toString().match(re);
|
|
return m ? parseFloat(m[1]) : value;
|
|
};
|
|
|
|
/**
|
|
* Round the number to a number of decimal in precision.
|
|
* @param {*} value the number value
|
|
* @param {*} decimals the number of decimal digits
|
|
* @returns Works just like Number.toFixed(decimals) but return a number instead.
|
|
*/
|
|
function roundTo(value, decimals = 2) {
|
|
if (undefined === value) return 0;
|
|
let _decs = decimals || 0;
|
|
return Number(Math.round(Number(value + 'e' + _decs)) + 'e-' + _decs);
|
|
}
|
|
|
|
function arrayToObject(array, keyField, makeKeyLowerCase = false) {
|
|
return !isEmptyArray(array) ? array.reduce((obj, item) => {
|
|
obj[makeKeyLowerCase ? item[keyField].toLowerCase() : item[keyField]] = item;
|
|
return obj;
|
|
}, {}) : null;
|
|
}
|
|
|
|
/**
|
|
* Capitalize first characters on every words
|
|
* @param {*} str input string
|
|
*/
|
|
function capitalize(str) {
|
|
return (str && str.length) ? str.trim().toLowerCase().replace(/\w\S*/g, (w) => (w.replace(/^\w/, (c) => c.toUpperCase()))) : str;
|
|
}
|
|
|
|
|
|
function objToStrArray(obj, fields, ops = { delimeter: ':', endEach: '', capitalize: true }) {
|
|
const strs = [];
|
|
const regex = fields && fields.length ? new RegExp(fields.join("|")) : null;
|
|
let theKey;
|
|
|
|
for (const [key, val] of Object.entries(obj)) {
|
|
theKey = ops.capitalize ? capitalize(key) : key;
|
|
if (val && typeof val != 'object') {
|
|
if (!regex || regex.test(key)) strs.push(`${theKey}${ops.delimeter} ${val}${ops.endEach}`);
|
|
}
|
|
}
|
|
return strs;
|
|
}
|
|
|
|
/**
|
|
* Determine two arrays equal values or not
|
|
* WARNING: arrays must not contain {objects} or behavior may be undefined
|
|
*/
|
|
function arraysEqual(ar1, ar2, sorted = false) {
|
|
if (sorted)
|
|
return (Array.isArray(ar1) && Array.isArray(ar2) && JSON.stringify(ar1.sort()) == JSON.stringify(ar2.sort()));
|
|
return JSON.stringify(ar1) == JSON.stringify(ar2);
|
|
}
|
|
|
|
function readMemUsage() {
|
|
const used = process.memoryUsage();
|
|
for (let key in used) {
|
|
debug(`${key} ${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB`);
|
|
}
|
|
}
|
|
|
|
function getProdUnit(unit) {
|
|
let res = '';
|
|
switch (Number(unit)) {
|
|
case 0:
|
|
res = 'oz';
|
|
break;
|
|
case 1:
|
|
res = 'gal';
|
|
break;
|
|
case 2:
|
|
res = 'lb';
|
|
break;
|
|
case 3:
|
|
res = 'lit';
|
|
break;
|
|
case 4:
|
|
res = 'kg';
|
|
break;
|
|
case 5:
|
|
res = 'gr';
|
|
break;
|
|
case 6:
|
|
res = 'cc';
|
|
break;
|
|
case 7:
|
|
res = 'pt';
|
|
break;
|
|
}
|
|
// if (value > 1) res += "s";
|
|
return res;
|
|
}
|
|
|
|
function padZero(num, size) {
|
|
let s = num + '';
|
|
while (s.length < size) {
|
|
s = '0' + s;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function toNumber(val, defaultVal) {
|
|
const _defaultVal = defaultVal || 0;
|
|
const numVal = Number(val);
|
|
return isNumber(numVal) ? numVal : _defaultVal;
|
|
}
|
|
|
|
function truncR(val, digits) {
|
|
let _val = Number(val) || 0;
|
|
return _val.toFixed(digits);
|
|
};
|
|
|
|
function timeToSeconds(hms) {
|
|
const a = hms.split(':');
|
|
return (+a[0]) * 60 * 60 + (+a[1]) * 60 + (+a[2]);
|
|
}
|
|
|
|
/**
|
|
* Get the Julian Date string (YYYYddd) or Julian days from the input Date String
|
|
* @param {*} dateStr standard date string
|
|
* @param {*} isUTC Whether the date string is UTC. Default true
|
|
*/
|
|
function julianDate(dateStr, isUTC = true) {
|
|
const dayCount = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
//convert passed string to date object
|
|
let dte = isUTC ? new moment.utc(dateStr) : new moment(dateStr);
|
|
if (!dte.isValid())
|
|
dte = isUTC ? new moment.utc() : new moment();
|
|
|
|
// Initialize date letiable
|
|
let julianDays = 0;
|
|
// Add days for previous months
|
|
for (let i = 0; i < dte.month(); i++) {
|
|
julianDays += dayCount[i];
|
|
}
|
|
// Add days of the current month
|
|
julianDays += dte.date();
|
|
if (dte.isLeapYear()) {
|
|
julianDays++;
|
|
}
|
|
return { dateStr: '' + dte.format('YYYY') + padZero(julianDays, 3), days: julianDays };
|
|
}
|
|
|
|
/**
|
|
* Convert seconds to string format
|
|
* @param {*} value
|
|
* @param {*} format 1: HH:MM:SS, others: H[h] MM[m] [SS]s
|
|
*/
|
|
function secondsToHMS(value, format = 1) {
|
|
const sec = parseInt(value, 10); // convert value to number if it's string
|
|
if (value <= 0) return '';
|
|
let hours = Math.floor(sec / 3600); // get hours
|
|
let minutes = Math.floor((sec - (hours * 3600)) / 60); // get minutes
|
|
let seconds = sec - (hours * 3600) - (minutes * 60); // get seconds
|
|
// add 0 if value < 10
|
|
if (format == 1 && hours < 10) { hours = "0" + hours; }
|
|
if (minutes < 10) { minutes = "0" + minutes; }
|
|
if (seconds < 10) { seconds = "0" + seconds; }
|
|
return format == 1 ? hours + ':' + minutes + ':' + seconds // Return is HH : MM : SS
|
|
: `${hours}h ${minutes}m ${seconds}s`;
|
|
}
|
|
|
|
function timestampToDate(ts, format = "YYYY/MM/DD z") {
|
|
if (!ts) return '';
|
|
return moment.utc(ts * 1e3).format(format);
|
|
}
|
|
|
|
/**
|
|
* Count the total different in months between two dates
|
|
* @param {*} date1
|
|
* @param {*} date2
|
|
* @returns number of months
|
|
*/
|
|
function monthsDiff(date1, date2) {
|
|
return (date2.getFullYear() - date1.getFullYear()) * 12 + (date2.getMonth() - date1.getMonth());
|
|
}
|
|
|
|
function setConnTimeout(time) {
|
|
let delay = typeof time === 'string'
|
|
? ms(time)
|
|
: Number(time || 5000);
|
|
|
|
return function (req, res, next) {
|
|
req.socket.setKeepAlive(true);
|
|
res.socket.setTimeout(delay);
|
|
next();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Offset the DateTime string with the number of hours (+/-)
|
|
*
|
|
* @param {any} dateTimeStr in YYYYMMDDTHHMMSS
|
|
* @param {any} offSetHrs number of hours (+/-)
|
|
* @returns offseted DateTime string in YYYYMMDDTHHMMSS format
|
|
*/
|
|
function offsetDateTimeString(dateTimeStr, offSetHrs) {
|
|
let dt = dateTimePartsFromAgNav(dateTimeStr, 2);
|
|
let utcDT = new moment.utc(dt.date + 'T' + dt.time);
|
|
utcDT.subtract(offSetHrs);
|
|
return utcDT;
|
|
}
|
|
|
|
function waitFor(timeoutms, testFn) {
|
|
let ok = false;
|
|
return new Promise((resolve, reject) => {
|
|
let interval = setInterval(function () {
|
|
ok = testFn();
|
|
console.log('checking');
|
|
if (ok) {
|
|
resolve(true);
|
|
clearInterval(interval);
|
|
}
|
|
else if ((timeoutms -= 100) < 0) {
|
|
reject('timeout');
|
|
}
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
function objectIdIn(ois, oid) {
|
|
if (isEmptyArray(ois) || !oid) return false;
|
|
return ois.some(id => id.equals(oid));
|
|
}
|
|
|
|
function escapeRegExp(string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
}
|
|
|
|
function toFixedNumberString(numString, digits = 2, base = 10) {
|
|
const pow = Math.pow(base, digits);
|
|
return (Math.round(Number(numString) * pow) / pow).toString();
|
|
}
|
|
|
|
function toFixedNumber(num, decimal = 2) {
|
|
return Bignumber(Bignumber(num).toFixed(decimal)).toNumber()
|
|
}
|
|
|
|
function getPercentValue(val) {
|
|
return Bignumber(val).dividedBy(100)
|
|
}
|
|
|
|
/** Generates a secure random password.
|
|
* @param {string} seedChars - A string containing the characters to use for generating the password. Defaults to a predefined set if not provided.
|
|
* @param {number} length - The desired length of the password. Defaults to 10 if not provided.
|
|
* @returns {string} A randomly generated secure password.
|
|
*/
|
|
function generateRandomPassword(seedChars, length) {
|
|
const _length = length || 10;
|
|
const _seedChars = seedChars && seedChars?.length > 9 ? seedChars : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?';
|
|
let password = '';
|
|
let randomBytes;
|
|
try {
|
|
randomBytes = crypto.randomBytes(_length);
|
|
} catch (error) {
|
|
throw new Error('Failed to generate random bytes: ' + error.message);
|
|
}
|
|
const seedLength = _seedChars.length;
|
|
for (let i = 0; i < _length; i++) {
|
|
password += _seedChars[randomBytes[i] % seedLength];
|
|
}
|
|
return password;
|
|
}
|
|
|
|
/**
|
|
* Extract creation timestamp from MongoDB ObjectId
|
|
* @param {ObjectId|string} objectId - MongoDB ObjectId
|
|
* @returns {Date} Creation timestamp of the document
|
|
*/
|
|
function getDateTSFromObjectId(objectId) {
|
|
if (!objectId) return null;
|
|
|
|
try {
|
|
const id = typeof objectId === 'string' ? new ObjectId(objectId) : objectId;
|
|
return id.getTimestamp();
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure the req object is valid for downstream mailer functions
|
|
* @param {*} req - The request object to validate/sanitize
|
|
* @returns {object} - A valid request object with proper structure
|
|
*/
|
|
function ensureValidReqObject(req) {
|
|
if (!req || typeof req !== 'object') {
|
|
return {}; // Create a minimal request object
|
|
}
|
|
return req;
|
|
}
|
|
|
|
module.exports = {
|
|
waitUntil, execAsync, rateUnitString, rateStringToCode, isUSRateUnit, areaUnitString, toArea, toVolume, toMetricVolume, deg2Compass, inCorF, isEmptyArray,
|
|
isEmpty: isEmptyStr, isEmptyObj, isBlank, isObjectId, stringToBoolean, noLinebreaks, noSpecialChars, trimStrTo, removeEndChars, normalizeName, toMeter, getFirstProp,
|
|
getProp, getPropNum, hasProperty, dateTimePartsFromAgNav, toUTCDateTime, isValidDate, datafileNameToAgnString, ArrayBuffertoBuffer, dynamicSort, bareFilename, delay,
|
|
julianDate, appendArray, getField, mpSecToKnot, chunkArray, isNumber, fixedTo, roundTo, arrayToObject, objToStrArray, arraysEqual, objectIdIn, acreToHa, haToAcre, toLocaleStr,
|
|
getProdUnit, ozToGal, padZero, download, toNumber, truncR, timeToSeconds, secondsToHMS, setConnTimeout, offsetDateTimeString, waitFor, monthsDiff, capitalize, split2nd, line2ndFields, rateInfoFromFileMeta, appRateFromFlowRate, toMetricRate, escapeRegExp, ensureString, timestampToDate, toFixedNumberString, toFixedNumber, getPercentValue,
|
|
generateRandomPassword, getDateTSFromObjectId, ensureValidReqObject,
|
|
// FOR DEBUG ONLY
|
|
printByProp, readMemUsage,
|
|
}
|