/*
* Author: Vlad Seryakov vseryakov@gmail.com
* backendjs 2018
*/
const lib = require(__dirname + '/../lib');
const util = require("util");
/**
* Return object type, try to detect any distinguished type
* @param {any} value
* @return {string} detected Javascript type name
* @memberof module:lib
* @method typeName
*/
lib.typeName = function(value)
{
if (value === null) return "null";
const t = typeof value;
if (t === "object") {
switch (value.constructor?.name) {
case "Error":
case "TypeError":
case "RangeError":
case "SystemError":
case "SyntaxError":
case "ReferenceError":
return "error";
case "Array":
case "Buffer":
case "Date":
case "RegExp":
case "Set":
case "Map":
case "WeakMap":
return value.constructor.name.toLowerCase();
}
}
return t;
}
/**
* Detect type from the value
* @param {any} val
* @return {string} detected library type name
* @memberof module:lib
* @method autoType
*/
lib.autoType = function(val)
{
return this.isNumeric(val) ? "number":
typeof val == "boolean" || val == "true" || val == "false" ? "bool":
typeof val == "string" ?
val[0] == "^" && val.slice(-1) == "$" ? "regexp":
val[0] == "[" && val.slice(-1) == "]" ? "js":
val[0] == "{" && val.slice(-1) == "}" ? "js":
val[0] == "," || val.endsWith(",") ? "list" :
val.includes("|") && !/[()[\]^$]/.test(val) ? "list": "" : "";
}
/**
* @param {any} val
* @return {object|undefined} itself if a regular object and not null
* @memberof module:lib
* @method isObject
*/
lib.isObject = function(val)
{
return this.typeName(val) === "object" && val || undefined;
}
/**
* @param {any} val
* @return {number|NaN} itself if the value is a number and not equal 0
* @memberof module:lib
* @method isNumber
*/
lib.isNumber = function(val)
{
return typeof val === "number" && !Number.isNaN(val) ? val : NaN;
}
/**
* @param {string} val
* @return {string} the string value or empty string if not a string
* @memberof module:lib
* @method isString
*/
lib.isString = function(val)
{
return typeof val == "string" ? val : "";
}
/**
* @param {string} func
* @return {function|undefined} the function itself or undefined if not a function
* @memberof module:lib
* @method isFunc
*/
lib.isFunc = function(func)
{
return typeof func == "function" && func || undefined;
}
/**
* @param {any} val
* @param {string} prefix
* @return {boolean} true if the value is prefixed
* @memberof module:lib
* @method isPrefix
*/
lib.isPrefix = function(val, prefix)
{
return typeof prefix == "string" && prefix &&
typeof val == "string" && val.substr(0, prefix.length) == prefix;
}
/**
* @param {any} str
* @return {string|undefined} the value represents an UUID
* @memberof module:lib
* @method isUuid
*/
lib.isUuid = function(str, prefix)
{
if (this.rxUuid.test(str)) {
if (typeof prefix == "string" && prefix) {
if (!str.startsWith(prefix)) return;
}
return str;
}
}
/**
* @param {string} str
* @return {string|undefined} the string iself if contains Unicode characters
* @memberof module:lib
* @method isUnicode
*/
lib.isUnicode = function(str)
{
return /[\u007F-\uFFFF]/g.test(str) ? str : undefined;
}
/**
* @param {any} val
* @return {boolean} true if a number is positive, i.e. greater than zero
* @memberof module:lib
* @method isPositive
*/
lib.isPositive = function(val)
{
return this.isNumber(val) > 0;
}
/**
* @param {any[]} val
* @param {any} dflt
* @return {any[]|undefined} the array if the value is non empty array or dflt value if given or undefined
* @memberof module:lib
* @method isArray
*/
lib.isArray = function(val, dflt)
{
return Array.isArray(val) && val.length ? val : dflt;
}
/**
* @param {any} val
* @return {boolean} true of the given value considered empty
* @memberof module:lib
* @method isEmpty
*/
lib.isEmpty = function(val)
{
switch (this.typeName(val)) {
case "null":
case "undefined":
return true;
case "buffer":
case "array":
return val.length === 0;
case "set":
case "map":
return val.size === 0;
case "number":
case "date":
return Number.isNaN(val);
case "regexp":
case "boolean":
case "function":
return false;
case "object":
for (const p in val) return false;
return true;
case "string":
return this.rxEmpty.test(val) ? true : false;
default:
return val ? false: true;
}
}
/**
* @param {any} val
* Return {boolean} true if the value is a number or string representing a number
* @memberof module:lib
* @method isNumeric
*/
lib.isNumeric = function(val)
{
if (typeof val == "number") return true;
if (typeof val != "string") return false;
return this.rxNumber.test(val);
}
/**
* @param {any} val
* @return {boolean} true if the given date is valid
* @memberof module:lib
* @method isDate
*/
lib.isDate = function(val)
{
return util.types.isDate(val) && !Number.isNaN(val.getTime());
}
/**
* @param {any[]} list
* @param {any|any[]} item
* @return {boolean} true if `item` exists in the array `list`, search is case sensitive. if `item` is an array it will return true if
* any element in the array exists in the `list`.
* @memberof module:lib
* @method isFlag
*/
lib.isFlag = function(list, item)
{
return Array.isArray(list) && (Array.isArray(item) ? item.some((x) => (list.includes(x))) : item && list.includes(item));
}
/**
* Evaluate an expr, compare 2 values with optional type and operation, compare a data value `val`` against a condtion `cond`.
* @param {any} val
* @param {object} condition
* @param {string} [op]
* @param {string} [type]
* @return {boolean} true if equal
* @memberof module:lib
* @method isTrue
*/
lib.isTrue = function(val, cond, op, type)
{
if (val === undefined && cond === undefined) return true;
if (val === null && cond === null) return true;
op = typeof op == "string" && op.toLowerCase() || "";
var no = false, yes = true, v1, list2;
if (op[0] == "n" && op[1] == "o" && op[2] == "t") no = true, yes = false;
switch (op) {
case "null":
if (val) return no;
break;
case "not null":
case "not_null":
if (val) return no;
break;
case ">":
case "gt":
if (this.toValue(val, type) <= this.toValue(cond, type)) return no;
break;
case "<":
case "lt":
if (this.toValue(val, type) >= this.toValue(cond, type)) return no;
break;
case ">=":
case "ge":
if (this.toValue(val, type) < this.toValue(cond, type)) return no;
break;
case "<=":
case "le":
if (this.toValue(val, type) > this.toValue(cond, type)) return no;
break;
case "between":
// If we cannot parse out 2 values, treat this as exact operator
list2 = this.split(cond);
if (list2.length > 1) {
if (this.toValue(val, type) < this.toValue(list2[0], type) || this.toValue(val, type) > this.toValue(list2[1], type)) return no;
} else {
if (this.toValue(val, type) != this.toValue(cond, type)) return no;
}
break;
case "in":
case "not_in":
case "not in":
if (!lib.isFlag(this.split(cond, null, { datatype: type }), this.split(val, null, { datatype: type }))) return no;
break;
case "all_in":
case "all in":
list2 = this.split(cond, null, { datatype: type });
if (!this.split(val, null, { datatype: type }).every((x) => (list2.includes(x)))) return no;
break;
case 'like%':
case "not like%":
case 'begins_with':
case 'not begins_with':
v1 = this.toValue(val);
if (this.toValue(cond).substr(0, v1.length) != v1) return no;
break;
case "ilike%":
case "not ilike%":
v1 = this.toValue(val).toLowerCase();
if (this.toValue(cond).substr(0, v1.length).toLowerCase() != v1) return no;
break;
case "ilike":
case "not ilike":
if (this.toValue(val).toLowerCase() != this.toValue(cond).toLowerCase()) return no;
break;
case "!~":
case "!~*":
case "iregexp":
case "not iregexp":
if (!util.types.isRegExp(cond)) cond = this.toRegexp(cond, "i");
if (!cond || !cond.test(val)) return no;
break;
case "~":
case "~*":
case "regexp":
case "not regexp":
if (!util.types.isRegExp(cond)) cond = this.toRegexp(cond);
if (!cond || !cond.test(val)) return no;
break;
case "contains":
case "not contains":
case "not_contains":
if (!this.toValue(cond).indexOf(this.toValue(val)) > -1) return no;
break;
case "!=":
case "<>":
case "ne":
if (this.toValue(val, type) == this.toValue(cond, type)) return no;
break;
default:
if (type == "list") {
if (!this.isFlag(this.split(cond), this.split(val))) return no;
} else
if (Array.isArray(cond)) {
if (!this.isFlag(cond, val)) return no;
} else
if (Array.isArray(val)) {
if (!this.isFlag(val, cond)) return no;
} else
if (util.types.isRegExp(cond)) {
if (!cond.test(val)) return no;
} else
if (this.toValue(val, type) != this.toValue(cond, type)) {
return no;
}
}
return yes;
}
/**
* @param {string} text
* @param {int} start
* @param {int} end
* @param {string} [delimiters] define a character set to be used for words boundaries, if not given or empty string the default will be used
* @return {boolean} true if it is a word at the position `start` and `end` in the `text` string,
* @memberof module:lib
* @method isWord
*/
lib.isWord = function(text, start, end, delimiters)
{
if (typeof text != "string") return false;
delimiters = typeof delimiters == "string" && delimiters || this.wordBoundaries;
if (start <= 0 || delimiters.includes(text[start - 1])) {
if (end + 1 >= text.length || delimiters.includes(text[end + 1])) {
return true;
}
}
return false;
}
/**
* Returns a score between 0 and 1 for two strings, 0 means no similarity, 1 means exactly similar.
* The default algorithm is JaroWrinkler, options.type can be used to specify a different algorithm:
* - sd - Sorensent Dice
* - cs - Cosine Similarity
* @memberof module:lib
* @method isSimilar
*/
lib.isSimilar = function(s1, s2, options)
{
if (!s1 || !s2 || !s1.length || !s2.length) return 0;
if (s1 === s2) return 1;
function SorensentDice(s1, s2) {
function getBigrams(str) {
var bigrams = [];
var strLength = str.length;
for (var i = 0; i < strLength; i++) bigrams.push(str.substr(i, 2));
return bigrams;
}
var l1 = s1.length-1, l2 = s2.length-1, intersection = 0;
if (l1 < 1 || l2 < 1) return 0;
var b1 = getBigrams(s1), b2 = getBigrams(s2);
for (let i = 0; i < l1; i++) {
for (let j = 0; j < l2; j++) {
if (b1[i] == b2[j]) {
intersection++;
b2[j] = null;
break;
}
}
}
return (2.0 * intersection) / (l1 + l2);
}
function CosineSimularity(s1, s2) {
function vecMagnitude(vec) {
var sum = 0;
for (var i = 0; i < vec.length; i++) sum += vec[i] * vec[i];
return Math.sqrt(sum);
}
var dict = {}, v1 = [], v2 = [], product = 0;
var f1 = s1.split(" ").reduce(function(a, b) { a[b] = (a[b] || 0) + 1; return a }, {});
var f2 = s2.split(" ").reduce(function(a, b) { a[b] = (a[b] || 0) + 1; return a }, {});
for (const key in f1) dict[key] = true;
for (const key in f2) dict[key] = true;
for (const term in dict) {
v1.push(f1[term] || 0);
v2.push(f2[term] || 0);
}
for (let i = 0; i < v1.length; i++) product += v1[i] * v2[i];
return product / (vecMagnitude(v1) * vecMagnitude(v2));
}
function JaroWrinker(s1, s2) {
var i, j, m = 0, k = 0, n = 0, l = 0, p = 0.1;
var range = (Math.floor(Math.max(s1.length, s2.length) / 2)) - 1;
var m1 = new Array(s1.length), m2 = new Array(s2.length);
for (i = 0; i < s1.length; i++) {
var low = (i >= range) ? i - range : 0;
var high = (i + range <= s2.length) ? (i + range) : (s2.length - 1);
for (j = low; j <= high; j++) {
if (!m1[i] && !m2[j] && s1[i] === s2[j]) {
m1[i] = m2[j] = true;
m++;
break;
}
}
}
if (!m) return 0;
for (i = 0; i < s1.length; i++) {
if (m1[i]) {
for (j = k; j < s2.length; j++) {
if (m2[j]) {
k = j + 1;
break;
}
}
if (s1[i] !== s2[j]) n++;
}
}
var weight = (m / s1.length + m / s2.length + (m - (n / 2)) / m) / 3;
if (weight > 0.7) {
while (s1[l] === s2[l] && l < 4) ++l;
weight = weight + l * p * (1 - weight);
}
return weight;
}
switch (options && options.type) {
case "sd":
return SorensentDice(s1, s2);
case "cs":
return CosineSimularity(s1, s2);
default:
return JaroWrinker(s1, s2);
}
}
/**
* @param {...any} args
* @return {number} first valid number from the list of arguments or 0
* @memberof module:lib
* @method isValidNum
*/
lib.validNum = function(...args)
{
for (const i in args) {
if (this.isNumber(args[i])) return args[i];
}
return 0;
}
/**
* @param {...any} args
* @return {number} first valid positive number from the list of arguments or 0
* @memberof module:lib
* @method isValidPositive
*/
lib.validPositive = function(...args)
{
for (const i in args) {
if (this.isPositive(args[i])) return args[i];
}
return 0;
}
/**
* @param {...any} args
* @return {boolean} first valid boolean from the list of arguments or false
* @memberof module:lib
* @method isValidBool
*/
lib.validBool = function(...args)
{
for (const i in args) {
if (typeof args[i] == "boolean") return args[i];
}
return false;
}
/**
* @param {string} version
* @param {string} [condition] - can be: >=M.N, >M.N, =M.N, <=M.N, <M.N, M.N-M.N
* @eturn {boolean} true if the version is within given condition(s), always true if either argument is empty.
* @memberof module:lib
* @method isValidVersion
*/
lib.validVersion = function(version, condition)
{
if (!version || !condition) return true;
version = typeof version == "number" ? version : lib.toVersion(version);
condition = lib.split(condition);
if (!condition.length) return true;
return condition.some((x) => {
const d = x.match(this.rxVersion);
if (!d) return false;
return d[3] ? lib.isTrue(version, [lib.toVersion(d[3]), lib.toVersion(d[4])], "between", "number") :
lib.isTrue(version, lib.toVersion(d[2]), d[1] || ">=", "number");
});
}