SmartComparer.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. // Smart comparison of three.js objects.
  2. // Identifies significant differences between two objects.
  3. // Performs deep comparison.
  4. // Comparison stops after the first difference is found.
  5. // Provides an explanation for the failure.
  6. function SmartComparer() {
  7. 'use strict';
  8. // Diagnostic message, when comparison fails.
  9. var message;
  10. // Keys to skip during object comparison.
  11. var omitKeys = [ 'id', 'uuid' ];
  12. return {
  13. areEqual: areEqual,
  14. getDiagnostic: function() {
  15. return message;
  16. }
  17. };
  18. // val1 - first value to compare (typically the actual value)
  19. // val2 - other value to compare (typically the expected value)
  20. function areEqual( val1, val2 ) {
  21. // Values are strictly equal.
  22. if ( val1 === val2 ) return true;
  23. // Null or undefined values.
  24. /* jshint eqnull:true */
  25. if ( val1 == null || val2 == null ) {
  26. if ( val1 != val2 ) {
  27. return makeFail( 'One value is undefined or null', val1, val2 );
  28. }
  29. // Both null / undefined.
  30. return true;
  31. }
  32. // Don't compare functions.
  33. if ( _.isFunction( val1 ) && _.isFunction( val2 ) ) return true;
  34. // Array comparison.
  35. var arrCmp = compareArrays( val1, val2 );
  36. if ( arrCmp !== undefined ) return arrCmp;
  37. // Has custom equality comparer.
  38. if ( val1.equals ) {
  39. if ( val1.equals( val2 ) ) return true;
  40. return makeFail( 'Comparison with .equals method returned false' );
  41. }
  42. // Object comparison.
  43. var objCmp = compareObjects( val1, val2 );
  44. if ( objCmp !== undefined ) return objCmp;
  45. // if (JSON.stringify( val1 ) == JSON.stringify( val2 ) ) return true;
  46. // Continue with default comparison.
  47. if ( _.isEqual( val1, val2 ) ) return true;
  48. // Object differs (unknown reason).
  49. return makeFail( 'Values differ', val1, val2 );
  50. }
  51. function compareArrays( val1, val2 ) {
  52. var isArr1 = Array.isArray( val1 );
  53. var isArr2 = Array.isArray( val2 );
  54. // Compare type.
  55. if ( isArr1 !== isArr2 ) return makeFail( 'Values are not both arrays' );
  56. // Not arrays. Continue.
  57. if ( !isArr1 ) return undefined;
  58. // Compare length.
  59. var N1 = val1.length;
  60. var N2 = val2.length;
  61. if ( N1 !== val2.length ) return makeFail( 'Array length differs', N1, N2 );
  62. // Compare content at each index.
  63. for ( var i = 0; i < N1; i ++ ) {
  64. var cmp = areEqual( val1[ i ], val2[ i ] );
  65. if ( !cmp ) return addContext( 'array index "' + i + '"' );
  66. }
  67. // Arrays are equal.
  68. return true;
  69. }
  70. function compareObjects( val1, val2 ) {
  71. var isObj1 = _.isObject( val1 );
  72. var isObj2 = _.isObject( val2 );
  73. // Compare type.
  74. if ( isObj1 !== isObj2 ) return makeFail( 'Values are not both objects' );
  75. // Not objects. Continue.
  76. if ( !isObj1 ) return undefined;
  77. // Compare keys.
  78. var keys1 = _( val1 ).keys().difference( omitKeys ).value();
  79. var keys2 = _( val2 ).keys().difference( omitKeys ).value();
  80. var missingActual = _.difference( keys1, keys2 );
  81. if ( missingActual.length !== 0 ) {
  82. return makeFail( 'Property "' + missingActual[0] + '" is unexpected.' );
  83. }
  84. var missingExpected = _.difference( keys2, keys1 );
  85. if ( missingExpected.length !== 0 ) {
  86. return makeFail( 'Property "' + missingExpected[0] + '" is missing.' );
  87. }
  88. // Keys are the same. For each key, compare content until a difference is found.
  89. var hadDifference = _.any( keys1, function ( key ) {
  90. var prop1 = val1[key];
  91. var prop2 = val2[key];
  92. // Compare property content.
  93. var eq = areEqual( prop1, prop2 );
  94. // In case of failure, an message should already be set.
  95. // Add context to low level message.
  96. if ( !eq ) addContext( 'property "' + key + '"' );
  97. return !eq;
  98. });
  99. return ! hadDifference;
  100. }
  101. function makeFail( msg, val1, val2 ) {
  102. message = msg;
  103. if ( arguments.length > 1) message += " (" + val1 + " vs " + val2 + ")";
  104. return false;
  105. }
  106. function addContext( msg ) {
  107. // There should already be a validation message. Add more context to it.
  108. message = message || "Error";
  109. message += ", at " + msg;
  110. return false;
  111. }
  112. }