Source

tools/Func.js

// //@ts-check

'use strict'

// $Id$

/**
 *  
 *  @category Tools
 * 
 *  @classdesc
 *      Helper functions 
 *
 */
export class Func {

    constructor() {
    }

    /**
     *
     * Sanitize ID
     *
     * Remove :, =, ' '
     *
     * @param {string} id
     * @returns {string}
     */
    static sanitize(id) {
        id = id.replace(/:/g, "")
        id = id.replace(/=/g, "")
        id = id.replace(/\s/g, "")
        return id
    }

    /**
     * Create a new ID
     * @returns {string} New ID
     */
    static createId () {
        return "id" + Math.random().toString(16).slice(2)
    }

    /**
     * Get number of bytes as formatted text in suitable units ['Bytes', 'kB', 'MB', 'GB']
     *
     * @param {number} size
     * @returns {string}
     */
    static getSizeAsString (size) {

        const u = ['Bytes', 'kB', 'MB', 'GB']

        let idx = 0

        while (size > 1024) {
            size /= 1024
            idx++
            if (idx >= (u.length - 1)) break;
        }

        return size.toFixed(idx) + " " + u[idx]

    }

    /**
     * Clone/Deep copy this object
     * @param {Object} obj
     * @returns {Object}
     */
    static clone (obj) {

        let copy;

        // Handle the 3 simple types, and null or undefined. Note that typeof null = 'object'
        if (null == obj || "object" != typeof obj) return obj;

        // Handle Date
        if (obj instanceof Date) {
            copy = new Date();
            copy.setTime(obj.getTime());
            return copy;
        }

        // Handle Array
        if (obj instanceof Array) {
            copy = [];
            for (let i = 0, len = obj.length; i < len; i++) {
                copy[i] = this.clone(obj[i]);
            }
            return copy;
        }

        // Handle Object
        if (obj instanceof Object) {
            copy = {};
            for (const attr in obj) {
                if (obj.hasOwnProperty(attr)) copy[attr] = this.clone(obj[attr]);
            }
            return copy;
        }

        throw new Error("Unable to copy obj! Unsupported type.");

    }

    /**
     * Count objects/keys in a dictionary.
     * @param {Object} obj
     * @returns {number} Number of 'own' properties
     */
    static count (obj) {

        return Object.keys(obj).length // get only own properties, not properties from the prototype chain!
        // var c = 0
        // for (var i in obj) c++
        // return c
    }

    /**
     * 
     * @param {*} str 
     * @param {*} olds 
     * @param {*} news 
     * @returns {string}
     */
    static replaceStr ( str, olds, news ) {

        let val = String(str)

        let m = val.split( olds )

        let e = ''
        let lb = ''

        for( let j in m ) {
            e += lb + m[j]
            lb = news
        }

        return e

    }

    /**
     * Compares content of two objects (yet not recursively for objects!)
     *
     * @param {Object} obj1
     * @param {Object} obj2
     * @param {boolean} [recursively] not implemented yet
     * @returns {boolean}
     */
    static compare (obj1, obj2, recursively = false) {

        if(obj1 == null && obj2 != null ) return false
        if(obj2 == null && obj1 != null ) return false
        if(obj2 == null && obj1 == null ) return true

        // check simple types
        if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
            if (obj1 === obj2) return true
            console.log('>>> compare: ' + obj1 + ' !== ' + obj2 + ' <<<')
            return false
        }

        // check arrays
        if (Array.isArray(obj1) && Array.isArray(obj2)) {
            return JSON.stringify(obj1) === JSON.stringify(obj2); // should be faster than recursive check?
        }

        // check dates
        if (obj1 instanceof Date && obj2 instanceof Date) {
            return obj1.getTime() === obj2.getTime();
        }

        // check properties
        // console.log("obj1: " + JSON.stringify(obj1))
        // console.log("obj2: " + JSON.stringify(obj2))
        const props1 = Object.getOwnPropertyNames(obj1);
        const props2 = Object.getOwnPropertyNames(obj2);

        if (props1.length !== props2.length) {
            return false;
        }

        // Compare the values of each property
        for (const prop of props1) {

            const val1 = obj1[prop]
            const val2 = obj2[prop]

            if (!this.compare(val1, val2)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Get tokens specified by start-end string in str
     * Examples start = '[[' , end = ']]'
     * @param {string} str
     * @param {string} start
     * @param {string} end
     * @returns {Array<string>} Array with extracted tokens, empty array if no tokens could be extractred
     */
    static getTokenList (str, start, end) {

        let tokenlist = []

        let token = ''

        for (let i = 0; i < str.length - 1; i++) {

            const g = str[i] + str[i + 1]

            if (g == start) {
                token = ''
                i++
            } else if (g == end) {
                // console.log("token: " + token)
                tokenlist.push(token)
                token = ''
            } else {
                token += str[i]
            }

        }

        return tokenlist

    }

    /**
     *
     * Create a md5 key from a given string
     *
     * @param {string} inputString
     * @returns {string} MD5 key
     */
    static md5 (inputString) {
        let hc = "0123456789abcdef";
        function rh(n) { var j, s = ""; for (j = 0; j <= 3; j++) s += hc.charAt((n >> (j * 8 + 4)) & 0x0F) + hc.charAt((n >> (j * 8)) & 0x0F); return s; }
        function ad(x, y) { var l = (x & 0xFFFF) + (y & 0xFFFF); var m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); }
        function rl(n, c) { return (n << c) | (n >>> (32 - c)); }
        function cm(q, a, b, x, s, t) { return ad(rl(ad(ad(a, q), ad(x, t)), s), b); }
        function ff(a, b, c, d, x, s, t) { return cm((b & c) | ((~b) & d), a, b, x, s, t); }
        function gg(a, b, c, d, x, s, t) { return cm((b & d) | (c & (~d)), a, b, x, s, t); }
        function hh(a, b, c, d, x, s, t) { return cm(b ^ c ^ d, a, b, x, s, t); }
        function ii(a, b, c, d, x, s, t) { return cm(c ^ (b | (~d)), a, b, x, s, t); }
        function sb(x) {
            var i; var nblk = ((x.length + 8) >> 6) + 1; var blks = new Array(nblk * 16); for (i = 0; i < nblk * 16; i++) blks[i] = 0;
            for (i = 0; i < x.length; i++) blks[i >> 2] |= x.charCodeAt(i) << ((i % 4) * 8);
            blks[i >> 2] |= 0x80 << ((i % 4) * 8); blks[nblk * 16 - 2] = x.length * 8; return blks;
        }
        var i, x = sb(inputString), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb, oldc, oldd;
        for (i = 0; i < x.length; i += 16) {
            olda = a; oldb = b; oldc = c; oldd = d;
            a = ff(a, b, c, d, x[i + 0], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819);
            b = ff(b, c, d, a, x[i + 3], 22, -1044525330); a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426);
            c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983); a = ff(a, b, c, d, x[i + 8], 7, 1770035416);
            d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
            a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290);
            b = ff(b, c, d, a, x[i + 15], 22, 1236535329); a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632);
            c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i + 0], 20, -373897302); a = gg(a, b, c, d, x[i + 5], 5, -701558691);
            d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848);
            a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961);
            b = gg(b, c, d, a, x[i + 8], 20, 1163531501); a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784);
            c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734); a = hh(a, b, c, d, x[i + 5], 4, -378558);
            d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556);
            a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632);
            b = hh(b, c, d, a, x[i + 10], 23, -1094730640); a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i + 0], 11, -358537222);
            c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189); a = hh(a, b, c, d, x[i + 9], 4, -640364487);
            d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651);
            a = ii(a, b, c, d, x[i + 0], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905);
            b = ii(b, c, d, a, x[i + 5], 21, -57434055); a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606);
            c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799); a = ii(a, b, c, d, x[i + 8], 6, 1873313359);
            d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
            a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259);
            b = ii(b, c, d, a, x[i + 9], 21, -343485551); a = ad(a, olda); b = ad(b, oldb); c = ad(c, oldc); d = ad(d, oldd);
        }
        return rh(a) + rh(b) + rh(c) + rh(d);
    }

    /**
     * Elide text
     *
     * @param {string} str - Text
     * @param {number} max_length - Maximum length of text
     * @returns {string|null} Center elided text or null if str = null
     */
    static elideText (str, max_length) {

        if( str == undefined ) {
            return null
        }

        if( typeof str != 'string' ) {
            return str
        }

        if (str.length <= max_length) {
            return str
        }

        const ellipsis = ' ... '

        if (max_length < ellipsis.length) {
            return str
        }

        const truncatedLength = Math.trunc(0.5 * (max_length - ellipsis.length))

        let s = str.substring(0, truncatedLength + (max_length + 1) % 2)
        s += ellipsis
        s += str.substring(str.length - truncatedLength)

        return s

    }

    /**
     * Encode text from text to JSON acceptable text.
     * So this text can be transferred and stored in a DB.
     *
     * \n   -> <br>
     * '    -> ```
     *
     * @param {string} str - Text to be encoded from JSON
     * @returns {string}
     */
    static txtToDb (str) {
        if( str == undefined ) return ''
        if( typeof str != 'string' ) str = String(str)

        str = str.replace(/\n\r?/g, '<br>')
        // str = str.replace( /\\n/g , '<br>' )
        str = str.replace(/'/g, '```')

        return str
    }

    /**
     *
     * Encode text from text to HTML acceptable text
     *
     * \n   -> <br>
     * ```  -> '
     *
     * @param {string} str
     * @returns {string}
     */
    static txtToHtml (str) {
        if( str == undefined ) return ''
        // str = str.replace( /\\n/g, '<br>')
        str = str.replace(/\n\r?/g, '<br>')
        str = str.replace(/```/g, "'")
        return str
    }

    /**
     *
     * @param {string} str - Text to be decoded from DB format to JSON.
     * @returns {string}
     */
    static dbToTxt (str) {
        if( str == undefined ) return ''
        str = str.replace(/<br>/g, '\n')
        str = str.replace(/```/g, "'")
        return str
    }

    /**
     * Convert a string which is stored in a db to a string
     * that can be rendered in html.
     *
     * ``` -> '
     *
     * @param {*} str
     * @returns {string}
     */
    static dbToHtml (str) {
        if( str == undefined ) return ''
        // str = str.replace(/```/g, "'")
        str = str.replace(/```/g, "&#39;" )
        return str
    }

    /**
     *
     * Sanitize string 'a' so it can be used in a html tooltip (title).
     *
     * @param {string} a
     * @returns {string}
     */
    static sanitizeTooltip( a ) {
        return a.replace(/&/g, '&amp;')
        .replace(/'/g, '&#39;')
        .replace(/"/g, '&quot;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;');
    }

    /**
     * Validates whether a text is a syntactically correct email address.
     * Note: The only way to reliably verify that a supplied email is a working valid email is to send a mail with a verification link
     *
     * @param {string} str - String holding the Email address
     * @returns {boolean} True, if validation is successful, else false
     */
    static validateEmail (str) {
        // version 1
        const regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/g
        // version 2 (RFC 5322 compliant)
        //const regex = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/g
        return regex.test(str)
    }

    /**
     * Check if pattern is a valid regex expression.
     *
     * @param {string} pattern
     * @returns {boolean}
     */
    static validateRegex (pattern) {

        let regex = pattern
        let options = ''

        if (!pattern.startsWith('/')) {
            return false
        }

        regex = pattern.slice(1)

        const lastPos = regex.lastIndexOf('/')
        if (lastPos >= 0) {
            options = regex.slice(lastPos + 1)
            regex = regex.slice(0, lastPos)
        }

        console.log("regex = " + regex)
        console.log("options = " + options)

        try {
            new RegExp(regex, options);
            return true;
        }
        catch (e) {
            console.log(e)
            return false;
        }
    }

    /**
     *
     * @param {string} wildcard
     * @returns {RegExp}
     */
    static wildcardToRegex(wildcard) {

        // escape special characters
        const escapedWildcard = wildcard.replace(/[.+^${}()|[\]\\]/g, '\\$&');

        // replace "*" with ".*" and "?" with "."
        const regexString = escapedWildcard
            .replace(/\*/g, '.*')
            .replace(/\?/g, '.');

        return new RegExp('^' + regexString + '$');
    }

    /**
     * Check if pattern is a string representation (JSON) of an array.
     *
     * @param {string} pattern
     * @returns {boolean}
     */
    static validateArray (pattern) {

        try {
            const tmpVal = JSON.parse(pattern)
            if (Array.isArray(tmpVal)) {
                return true
            }
        } catch (e) {
            console.log(e)
        }

        return false
    }

    /**
     * Remove 'invalid'/invisible characters from string which can occur when copy-pasting from html pages
     * Todo: better Black- or Whitelist? Ranges?
     *
     * @param {string} str
     * @returns {string} Sanitized string
     */
    static sanitizeText (str) {

        str = str.replaceAll('\u200A', '')  // HSP
        str = str.replaceAll('\u200B', '')  // ZwSP
        str = str.replaceAll('\u200C', '')  // ZwNJ
        str = str.replaceAll('\u200D', '')  // ZwJ
        str = str.replaceAll('\u00A0', ' ') // Non-Breaking Space
        str = str.replaceAll('\u00AD', '')  // Soft Hyphen

        // ...

        return str
    }

    /**
     * Convert hmtl to plain text.
     *
     * @param {string} html
     * @param {boolean} all if true, get full text content (faster), else if slow get inner text (considering styles like display: none, slightly slower)
     * @returns {string} Plain text, empty string if html is invalid
     */
    static htmlToText (html, all) {

        const tmpDiv = document.createElement('div')
        tmpDiv.innerHTML = html

        let txt = all ? tmpDiv.textContent : tmpDiv.innerText
        if (!txt) txt = ''
        tmpDiv.remove()

        return txt
    }

    /**
     *
     * Compile filter string from id, type, name pattern and properties
     *
     * @param {*} projectlist       can be empty
     * @param {*} cataloguelist     can be empty
     * @param {*} constraints = {
     *      'pattern_object_id'     : ' ... ' // AND
     *      'pattern_object_name'   : ' ... ' // AND
     *      'pattern_object_type'   : ' ... ' // AND
     * }
     * @param {*} property_typeids  Search in these property types
     * @param {*} ptr_property      Search in each property type for this pattern
     *
     * @returns Filterstring according to web service
     *
     */
    static compileFilterString ( projectlist, cataloguelist , constraints , property_typeids, ptr_property) {

        let f = ''


        let cfg = new Object(constraints)
        let pattern_property = ptr_property

        if (cfg.pattern_object_id) {
            cfg.pattern_object_id = cfg.pattern_object_id.replace(/'/g, "\\'")
            cfg.pattern_object_id = cfg.pattern_object_id.replace(/\*/g, "%")
            cfg.pattern_object_id = cfg.pattern_object_id.replace(/"/g, "\\'")
        }

        if (cfg.pattern_object_name) {
            cfg.pattern_object_name = cfg.pattern_object_name.replace(/'/g, "\\'")
            cfg.pattern_object_name = cfg.pattern_object_name.replace(/\*/g, "%")
            cfg.pattern_object_name = cfg.pattern_object_name.replace(/"/g, "\\'")
        }

        if (pattern_property) {
            pattern_property = pattern_property.replace(/'/g, "\\'")
            pattern_property = pattern_property.replace(/\*/g, "%")
            pattern_property = pattern_property.replace(/"/g, "\\'")
        }

        let con = ''

        let c1 = ' ( '
        let c2 = ''

        if (projectlist?.length > 0) {

            if (projectlist.length = 1) {
                f += c1 + "$project ~eq~ '" + projectlist[0] + "' "
            } else {
                f += c1 + "$project ~in~ " + JSON.stringify(projectlist)
            }

            c1 = ' $AND '
            c2 = ' ) '

            con = ' $AND '

        } else if (cataloguelist?.length > 0) {

            if (cataloguelist.length = 1) {
                f += c1 + "$catalogue ~eq~ '" + cataloguelist[0] + "' "
            } else {
                f += c1 + "$catalogue ~in~ " + JSON.stringify(cataloguelist)
            }

            c1 = ' $AND '
            c2 = ' ) '

            con = ' $AND '

        }

        if (cfg.pattern_object_id?.length > 0) {

            let op = '~eq~'

            if (cfg.pattern_object_id.indexOf('%') >= 0) {
                op = '~like~'
            }

            f += c1 + " $object_id " + op + " \'" + cfg.pattern_object_id + "\' " //    $OR $object_name " + op + "\'" + pattern_object_id + " ) "

            c1 = ' $AND '
            c2 = ' ) '

            con = ' $AND '

        }

        if (cfg.pattern_object_name?.length > 0) {

            let op = '~eq~'

            if (cfg.pattern_object_name.indexOf('%') >= 0) {
                op = '~like~'
            }

            f += c1 + " $object_name " + op + " \'" + cfg.pattern_object_name + "\' "

            c2 = ' ) '
            con = ' $AND '

        }

        if ( cfg.pattern_object_type ) {

            if (Array.isArray(cfg.pattern_object_type)) {

                if( cfg.pattern_object_type.length > 0 ) {

                    let ll = "'" + cfg.pattern_object_type[0] + "'"

                    for( let i=1; i<cfg.pattern_object_type.length; i++ ) {
                        ll += ",'" + cfg.pattern_object_type[i] + "'"
                    }

                    f += c1 + " $object_type ~in~ [" + ll + "] "

                }


            } else {

                if (cfg.pattern_object_type) {
                    cfg.pattern_object_type = cfg.pattern_object_type.replace(/'/g, "\\'")
                    cfg.pattern_object_type = cfg.pattern_object_type.replace(/\*/g, "%")
                    cfg.pattern_object_type = cfg.pattern_object_type.replace(/"/g, "\\'")
                }

                let op = '~eq~'

                if (cfg.pattern_object_type.indexOf('%') >= 0) {
                    op = '~like~'
                }

                f += c1 + " $object_type " + op + " \'" + cfg.pattern_object_type + "\' "

            }


            c2 = ' ) '
            con = ' $AND '

        }

        f += c2

        if (property_typeids?.length > 0) {

            f += con + ' ( '
            con = ''

            let patternlist = pattern_property.split(' ')

            for (let i = 0; i < property_typeids.length; i++) {

                let ptype = property_typeids[i]

                ptype = '$' + ptype.trim()

                // console.log("Ptype = [" + ptype + "]" )

                let f_and = ''
                let f_or = ''
                let con_and = ''
                let con_or = ''

                f += con + ' ( '

                for (let j = 0; j < patternlist.length; j++) {

                    let pat = patternlist[j]
                    pat = pat.trim()

                    // console.log("pat = [" + pat + "]")

                    if (pat.length < 1) continue

                    let op = ' ~eq~ '

                    if (pat.indexOf('%') >= 0) {
                        op = ' ~like~ '
                    }

                    if (pat[0] == '+') {

                        pat = pat.substring(1)

                        f_and += con_and + ptype + op + "'" + pat + "'"
                        con_and = ' $AND  '

                    } else {

                        f_or += con_or + ptype + op + "'" + pat + "'"
                        con_or = ' $OR '

                    }

                }

                if (f_or.length > 0) {

                    f += " ( " + f_or + " ) "

                    if (f_and.length > 0) {
                        f += ' $AND '
                    }

                }

                f += f_and

                f += " ) "
                con = ' $OR '

            }

            f += ' ) '

        }

        console.log("=== > FILTER return [" + f + "]")

        return f

    }

    /**
     * 
     * @param {*} b64 
     * @returns {string}
     */
    static atou (b64) {
        return decodeURIComponent(escape(atob(b64)))
    }

    /**
     * 
     * @param {*} data 
     * @returns  {string}
     */
    static utoa (data) {
        return btoa(unescape(encodeURIComponent(data)));
    }

    /**
     * 
     * @param {*} v 
     * @returns {boolean}
     */
    static isValue (v) {
        if (v == undefined) return false
        if (v == './.') return false
        if (v == '.*.') return false
        if (v == '...') return false
        return true
    }

    /**
     *
     * @param {*} val1
     * @param {*} val2
     * @param {Object} propertytype
     * @param {Object} options
     * {
     *      precision: 0.0001,
     *      case_sensitive: true
     * }
     * @returns {boolean}
     */
    static isEqual (val1, val2, propertytype, options = null) {

        if (!propertytype) return false

        if (propertytype.datatype == 'xs:double' || propertytype.datatype == 'xs:double') {
            const precision = options?.precision || 0.0001
            let v1 = parseFloat(val1)
            let v2 = parseFloat(val2)
            if (Math.abs(v1 - v2) < precision) return true
            return false
        } else if (propertytype.datatype == 'xs:boolean') {
            let f = [false, 'false', 0, '0'] // all numbers != 0 -> true
            return (f.includes(val1) === f.includes(val2))
        }

        if (options?.case_sensitive == false && typeof val1 == 'string' && typeof val2 == 'string') {
            return (val1.toLower() == val2.toLower())
        }
        return (val1 == val2)
    }

    /**
     * Check if a property value matches the given values.
     *
     * @param {any} value           Value of property. If null or undefined, false will be returned.
     * @param {string} xsdatatype   Explicit data type of value ('xs:...')
     * @param {*} matchvalue        Values : '*', ["A","B", "C"], [1,2,3], /^abc/g, == 'Text', > 1.234
     * @param {Object} propertymap  Evaluated values for property references [[name##type]]
     *      {
     *          'Text##xs:string'   : "ABC",
     *          'Number##xs:double' : 1.234
     *      }
     * @returns {boolean} true if match was successful, else false
     */
    static matchValue(value, propertytype, matchvalue, propertymap = null) {

        // console.log("")
        // console.log("VALUE: " + v )
        // console.log("MATCH: " + matchvalue )
        // console.log("TYPE:  " + propertytype )

        if (value == undefined) {
            return (matchvalue === '!*')
        }

        if (matchvalue === '*') {
            return true
        }

        let check = false

        let isNumber = false;

        const ptype_number = ['xs:double', 'xs:float', 'xs:int', 'xs:long']
        const ptype_string = ['xs:string', 'xs:ID', 'xs:IDREF', 'xs:anyURI']

        if (ptype_number.indexOf(propertytype) > -1) {

            isNumber = true

            value = parseFloat(value).toFixed(5)

            if (Array.isArray(matchvalue)) {
                for (const i in matchvalue) matchvalue[i] = parseFloat(matchvalue[i]).toFixed(5)
            }

        }


        if (Array.isArray(matchvalue)) { // check containment in list ["ABC", "DEF"] or [1.23456, 5.00000. -9.10000]

            check = (matchvalue.indexOf(value) >= 0)

        } else if (matchvalue.startsWith("/")) { // check RegExp

            let pattern = matchvalue
            let options = ''

            if (pattern.startsWith('/')) {

                pattern = pattern.slice(1)
                const pos = pattern.lastIndexOf('/')
                if (pos > -1) {
                    options = pattern.slice(pos + 1)
                    pattern = pattern.slice(0, pos)
                }
            }

            let r = null

            try {
                r = new RegExp(pattern, options)
            } catch (e) {
                console.log(e)
            }

            if (r) {
                check = (value.match(r) != null)
            } else {
                check = false
            }

            // console.log("=> regex, return " + check )
            // console.log("")

        } else {

            // console.log("is number? " + isNumber)

            if (isNumber) {

                if (isNaN(matchvalue) == true) { // isNaN erwartet eigentlich den Datentyp number als Parameter, funktioniert aber auch für nicht-leere Strings wie '[[.]] > 5'

                    let t = ''

                    if (matchvalue.indexOf('[[.]]') >= 0) {

                        t = "( " + matchvalue + " )"
                        t = t.replaceAll('[[.]]', value)

                        // "[[.]] > 1 && [[.]] < 5"
                        // => "(value > 1 && value < 5)"

                    } else {

                        t = "(" + value + " " + matchvalue + ")"

                        // "5"
                        // => "(value 5)"

                    }

                    // console.log("eval : " + t)

                    if (propertymap) {
                        for (let p in propertymap) {
                            t.replaceAll('[[' + p + ']]', propertymap[p])
                        }
                    }

                    try {

                        ///////////////
                        check = window.eval(t)
                        ///////////////

                    } catch (e) {
                        console.log(e)
                    }

                } else {
                    check = (Math.abs(matchvalue - value) < 0.0001)
                    // console.log("check: " + matchvalue + " - " + value + " = " + (matchvalue-value) )
                }

            } else {

                let t = ''

                if (matchvalue.indexOf('[[.]]') >= 0) {

                    t = "( " + matchvalue + " )"

                    if (ptype_string.indexOf(propertytype) >= 0) {
                        t = t.replaceAll('[[.]]', "'" + value + "'")
                    } else {
                        t = t.replaceAll('[[.]]', value)
                    }

                } else {

                    if (ptype_string.indexOf(propertytype) >= 0) {
                        value = "'" + value + "'" // 'Textwert' == 'Vergleichszeichenkette'
                    }

                    t = "(" + value + " " + matchvalue + ")"

                }

                if (propertymap) {
                    for (let p in propertymap) {
                        t.replaceAll('[[' + p + ']]', propertymap[p])
                    }
                }

                // console.log("eval : " + t)

                try {
                    // eval() should not be used with bundlers
                    // => Using direct eval with a bundler is not recommended and may cause problems [direct-eval]

                    ///////////////
                    check = window.eval(t)
                    ///////////////
                    
                } catch (e) {
                    console.log(e)
                }
            }

        }

        // console.log("return " + check )
        // console.log("")

        return check
    }

    /**
     * Convert component (R,G or B) to hex value.
     * 0    => 0
     * 255  => FF
     *
     * @param {number} c
     * @returns {string}
     */
    static componentToHex (c) {
        const hex = c.toString(16);
        return hex.length == 1 ? "0" + hex : hex;
    }

    /**
     *
     * Convert (RGB) color to hex value
     *
     * (0,0,0) => #000000
     * (255,255,255) => #FFFFFF
     *
     * @param {*} r
     * @param {*} g
     * @param {*} b
     * @returns {string}
     */
    static rgbToHex (r, g, b) {
        return "#" + cn_func.componentToHex(r) + cn_func.componentToHex(g) + cn_func.componentToHex(b);
    }

    /**
     * 
     * @param {*} a 
     * @param {*} b 
     * @returns {boolean}
     */
    static xor( a , b ) {
        return ( a || b ) && !( a && b )
    }

    /**
     *
     * Increase Index given as A0, A1, ... , B0, B1, ...
     *
     * @param {*} str_number
     * @returns {string}
     */
    static increaseIndex ( str_number ) {

        console.log('---')
        str_number = String(str_number)

        if( str_number == '' ) {
            console.log('retunnnn')
            return "A0"
        }

        // Separate the letter and number parts
        let letter = str_number[0];
        let number = parseInt( str_number.slice(1), 10 )

        // Increment the number
        number++;

        // If the number reaches 10, reset to 0 and increment the letter
        if (number === 10) {
            number = 0;
            letter = String.fromCharCode(letter.charCodeAt(0) + 1);
        }

        // Return the next value in the format "A0", "A1", ..., "B0", etc.
        console.log( "return : " + `${letter}${number}` )
        return `${letter}${number}`

    }

    /**
     * 
     * @returns {string}
     */
    static createTimestamp () {

        const now = new Date();

        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-based
        const day = String(now.getDate()).padStart(2, '0');

        const hours = String(now.getHours()).padStart(2, '0');
        const minutes = String(now.getMinutes()).padStart(2, '0');
        const seconds = String(now.getSeconds()).padStart(2, '0');

        return `${year}${month}${day}_${hours}${minutes}${seconds}`;

    }

    /**
     * replaces each character of !"#$%&'()*+,./:;<=>?@[\]^`{|}~ with _<ASCIcode>_. So jQuery can work with it.
     * see jquery docs: https://api.jquery.com/category/selectors/
     * Escaping with \\ as recommended there didn't work.
     * @param {String} id any String used as id with jquery
     * @returns {String} id but each character of !"#$%&'()*+,./:;<=>?@[\]^`{|}~ replaced by _<ASCIcode>_
     *
     */
    static replaceSpecialCharsWithASCII(id){
        if (!id) return id

        const specialChars = /[!"#$%&'()*+,./:;<=>?@[\]^` {|}~]/g
        return id.replace(specialChars, match => {
            return `_${match.charCodeAt(0)}_`;
        })
    }

}