codenautas/discrepances

View on GitHub
lib/discrepances.js

Summary

Maintainability
D
2 days
Test Coverage
"use strict";

(function codenautasModuleDefinition(root, name, factory) {
    /* global define */
    /* istanbul ignore next */
    if(typeof root.globalModuleName !== 'string'){
        root.globalModuleName = name;
    }
    /* istanbul ignore next */
    if(typeof exports === 'object' && typeof module === 'object'){
        module.exports = factory();
    }else if(typeof define === 'function' && define.amd){
        define(factory);
    }else if(typeof exports === 'object'){
        exports[root.globalModuleName] = factory();
    }else{
        root[root.globalModuleName] = factory();
    }
    root.globalModuleName = null;
})(/*jshint -W040 */this, 'discrepances', function() {
/*jshint +W040 */

/*jshint -W004 */
var discrepances = {};
/*jshint +W004 */

/*eslint global-require: 0*/
var bestGlobals = require('best-globals');

var datetime = bestGlobals.datetime;
var timeInterval = bestGlobals.timeInterval;
var changing = bestGlobals.changing;

function constructorName(object, opts){
    var name = bestGlobals.constructorName(object);
    return name==='anonymous' && !(opts||{}).distinguishAnonymous ? 'Object' : name;
}

function getType(variable) {
    if(null === variable) { return 'null'; }    
    return typeof variable;
}

function getClassOnlyForSomeOfBuiltIns(variable, opts) {
    if(variable instanceof Date  ) { return 'Date';   }
    if(variable instanceof RegExp) { return 'RegExp'; }
    if(variable instanceof Array ) { return 'Array';  }
    if(variable instanceof Error ) { return 'Error';  }
    if(variable instanceof Object) { return 'Object'; }
    /*eslint-disable no-proto */
    if(getType(variable)==='object' && ! variable.__proto__) { // jshint ignore:line
        return opts.duckTyping?'Object':'#null__proto__';
    }
    /*eslint-enable no-proto */
    throw new Error('undetected class type:' + typeof variable);
}

function timeStr(dt) { return datetime.ms(dt).toYmdHmsM().substr(11); }

function compareStrings(a,b) {
    var pos=0;
    var rPos=0;
    while(pos<Math.min(a.length,b.length) && a[pos]==b[pos]){ pos++; }
    while(rPos>pos-Math.min(a.length,b.length) && a[a.length+rPos-1] == b[b.length+rPos-1]){ rPos--; }
    var answer={
        differences:[a.substring(pos, a.length+rPos), b.substring(pos, b.length+rPos), {pos:pos}],
        values:[a,b]
    };
    if(rPos){
        answer.differences[2].rPos = rPos;
    }
    return answer;
}

function objectWithProp(key, val) {
    var obj = {};
    obj[key] = val;
    return obj;
}

function compareArrayElem(resultArray, a, b, index, opts) {
    var diff = nestedObject(a[index], b[index], opts);
    resultArray.push(diff ? objectWithProp(index, diff) : null);
}

function compareArrays(a, b, opts) {
    var rv = {};
    var res=[];
    var max = Math.min(a.length, b.length);
    for(var i_ab=0; i_ab<max; ++i_ab){ compareArrayElem(res, a, b, i_ab, opts); }
    if(a.length !== b.length) { rv.length = nestedObject(a.length,b.length,opts); }
    res.forEach(function(r,index) {
        if(r && (index in r)) {
            rv[index] = r[index];
        }
    });
    if(rv.length) {
        return {array:rv};
    } else {
        var diffs={};
        for(var p in rv) { if(rv.hasOwnProperty(p)){ diffs[p] = rv[p]; } }
        if(Object.keys(diffs).length) { return {array:diffs}; }
    }
    return null;
}

function compareDates(a, b) {
    if(a.getTime()===b.getTime()){
        var co = compareObjects(a,b,{unordered:true});
        if(co){
            return {date:co.object};
        }
        return null;
    }
    var res = [];
    var timesDiffer = timeStr(a) !== timeStr(b);
    var msDiff = a.getTime()-b.getTime();
    var showTimeDiff = Math.abs(msDiff) <= 359999000; // 99 * 60 * 60 * 1000 + 59 * 60 * 1000 + 59 * 1000;
    if(datetime.ms(a).toYmd() !== datetime.ms(b).toYmd()) {
        // aas[0]=ymd, aas[1]=hmsm
        var aas = datetime.ms(a).toYmdHmsM().split(' ');
        var bbs = datetime.ms(b).toYmdHmsM().split(' ');
        res.push(aas[0]);
        if(timesDiffer) {
            res.push(' ');
            res.push(aas[1].substr(0, a.getMilliseconds()?12:8));
        }
        res.push(' != ');
        res.push(bbs[0]);
        if(timesDiffer) {
            res.push(' ');
            res.push(bbs[1].substr(0, b.getMilliseconds()?12:8));
        }
        if(showTimeDiff && timesDiffer) { res.push(' => '); }
    }
    if(timesDiffer && showTimeDiff) {
        res.push(timeInterval({ms:msDiff}).toHms());
        var ms = Math.floor(Math.abs(msDiff) % 1000);
        if(ms>0) { res.push('.'+ms); }
    }
    return {difference:res.join(''), values:[a,b]};
}

function setPropertyIf(object, key, value, optionalCond) {
    if(optionalCond || value) { object[key] = value; }
}

function compareObjects(a, b, opts, hiddenAttrs) {
    var rv = {};
    var isOfClassError = !!hiddenAttrs;
    hiddenAttrs=hiddenAttrs||[];
    var aKeys = hiddenAttrs.concat(Object.keys(a));
    var bKeys = hiddenAttrs.concat(Object.keys(b));
    if(opts.unordered) {
        aKeys.forEach(function(key) { 
            setPropertyIf(rv, key, !(key in b) && !isOfClassError && (!opts.notMemberAsUndefined || a[key] != undefined) ? {onlyLeft:a[key]} : nestedObject(a[key], b[key], opts));
        });
        bKeys.forEach(function(key) {
            setPropertyIf(rv, key, !(key in a) && !isOfClassError && (!opts.notMemberAsUndefined || a[key] != undefined) ? {onlyRight:b[key]} : nestedObject(a[key], b[key], opts));
        });
        return Object.keys(rv).length ? (isOfClassError ? {error:rv} : {object:rv}) : null;
    } else {
        var diffs = {};
        aKeys.forEach(function(key,index) {
            var dif = {};
            var bKey = bKeys[index];
            var compKey = b[bKey];
            if(key !== bKey) {
                dif.keys = [key, bKey];
            } else {
                compKey = b[key];
            }
            setPropertyIf(dif, 'values', nestedObject(a[key], compKey, opts));
            if(dif.keys || dif.values) { diffs[index] = dif; }
        });
        // setPropertyIf(rv, 'differences', diffs, diffs.length);
        return Object.keys(diffs).length ?  {ord_object:diffs} : null;
    }
}

function compareClasses(classType, a, b, opts) {
    switch(classType) {
        case 'Array': return compareArrays(a, b, opts);
        case 'Date': return compareDates(a, b);
        case 'Object': return compareObjects(a, b, opts);
        case 'Error': return compareObjects(a, b, opts, ['message','fileName','lineNumber','name']);
    }
}

function compareFunctions(a, b, opts) {
    var as = a.toString();
    var bs = b.toString();
    if(as === bs) { return null; }
    return compareStrings(as, bs);
}

var defaultOpts = {
    unordered:true,
    duckTyping:false,
    autoTypeCast:false,
    deltaNumber:0
};

function DiscrepancesTester(tester){
    this.test = tester;
    this.message = bestGlobals.functionName(tester);
}

function getName(type, variable, clase, opts) {
    return type==='object' && variable.constructor ? constructorName(variable, opts):clase;
}

function isNotContainerType(classType) {
    return ! classType.match(/^(array|object)$/i);
}

function compare(typeOfObjects, keyA, keyB, valA, valB) {
    var r={};
    r[typeOfObjects] = [keyA,keyB];
    if(isNotContainerType(keyA) && isNotContainerType(keyB)) {
        r.values = [valA,valB];
    }
    return r;
}

function nestedObject(a, b, opts){
    opts = changing(defaultOpts, opts||{});
    if(a === b){ return null; }
    /*eslint-disable eqeqeq */
    if(opts.autoTypeCast && a == b){ return null; }
    /*eslint-enable eqeqeq */
    if(b instanceof DiscrepancesTester){ return b.test(a)?null:{fail: b.message}; }
    var typeA = getType(a);
    var typeB = getType(b);
    if(opts.deltaNumber && (typeA === 'number' && typeB === 'number' || typeA === 'number' && !isNaN(b) || typeB === 'number' && !isNaN(a))){
        if(opts.deltaNumber && Math.abs(a-b)<opts.deltaNumber){
            return null;
        }
    }
    if(typeA === typeB) {
        switch(typeA) {
            case 'number': return {difference:a-b, values:[a,b]};
            case 'string': return compareStrings(a, b);
            case 'boolean': return {values:[a, b]};
            case 'function': return compareFunctions(a, b, opts);
        }
        var classA = getClassOnlyForSomeOfBuiltIns(a, opts);
        var classB = getClassOnlyForSomeOfBuiltIns(b, opts);
        var nameA = getName(typeA,a,classA,opts);
        var nameB = getName(typeB,b,classB,opts);
        if(classA !== classB) {
            return compare('classes',nameA,nameB,a,b);
        } else if(!opts.duckTyping && nameA !== nameB /*&& a.constructor !== b.constructor*/) {
            return {classes:[nameA,nameB]};
        } else {
            return compareClasses(classA, a, b, opts);  
        }
    } else {
        return compare('types',typeA,typeB,a,b);
    }
}

function keying(falto, object, prefix){
    var isContainer=false;
    [{
        name:'array',
        left:"[",
        right:"]"
    },{
        name:'object',
        left:".",
        right:""
    },{
        name:'error',
        left:".",
        right:""
    },{
        name:'ord_object',
        left:"{",
        right:"}"
    }].forEach(function(container){
        if(object && container.name in object){
            var innerObject=object[container.name];
            for(var attr in innerObject){
                if(innerObject.hasOwnProperty(attr)){
                    keying(falto, innerObject[attr], prefix+container.left+attr+container.right);
                }
            }
            isContainer=true;
        }
    });
    if(!isContainer){
        if(object!=null){
            falto[prefix]=object;
        }
    }
}

discrepances = function discrepances(){
    var message="DEPRECATED! Discrepances is no more a function. Use discprepances.showAndThrow or discrepances.nestedObject";
    console.log(message);
    throw new Error(message);
};

discrepances.flatten = function flatten(a, b, opts){
    var nested = nestedObject(a, b, opts);
    var falto={};
    keying(falto, nested, "");
    return falto;
};

discrepances.showAndThrow = function showAndThrow(a, b, opts){
    var keyDiffs = discrepances.flatten(a, b, opts);
    var firstError;
    for(var attr in keyDiffs){
        if(keyDiffs.hasOwnProperty(attr)){
            if(!firstError){
                try{
                    firstError=attr+":"+JSON.stringify(keyDiffs[attr]);
                }catch(err){
                    firstError=attr+":"+keyDiffs[attr];
                }
            }
            console.log(attr, keyDiffs[attr]);
        }
    }
    if(firstError){
        if(opts && opts.showContext){
            console.log('context:',opts.showContext);
        }
        throw new Error("discrepances in "+firstError);
    }
};

discrepances.nestedObject = nestedObject;

discrepances.test = function discrepancesTest(tester){
    return new DiscrepancesTester(tester);
};

discrepances.defaultOpts = defaultOpts;

return discrepances;

});