SmartComparer.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. return {
  11. areEqual: areEqual,
  12. getDiagnostic: function () {
  13. return message;
  14. }
  15. };
  16. // val1 - first value to compare (typically the actual value)
  17. // val2 - other value to compare (typically the expected value)
  18. function areEqual( val1, val2 ) {
  19. // Values are strictly equal.
  20. if ( val1 === val2 ) return true;
  21. // Null or undefined values.
  22. /* jshint eqnull:true */
  23. if ( val1 == null || val2 == null ) {
  24. if ( val1 != val2 ) {
  25. return makeFail( 'One value is undefined or null', val1, val2 );
  26. }
  27. // Both null / undefined.
  28. return true;
  29. }
  30. // Don't compare functions.
  31. if ( isFunction( val1 ) && isFunction( val2 ) ) return true;
  32. // Array comparison.
  33. var arrCmp = compareArrays( val1, val2 );
  34. if ( arrCmp !== undefined ) return arrCmp;
  35. // Has custom equality comparer.
  36. if ( val1.equals ) {
  37. if ( val1.equals( val2 ) ) return true;
  38. return makeFail( 'Comparison with .equals method returned false' );
  39. }
  40. // Object comparison.
  41. var objCmp = compareObjects( val1, val2 );
  42. if ( objCmp !== undefined ) return objCmp;
  43. // if (JSON.stringify( val1 ) == JSON.stringify( val2 ) ) return true;
  44. // Object differs (unknown reason).
  45. return makeFail( 'Values differ', val1, val2 );
  46. }
  47. function isFunction( value ) {
  48. // The use of `Object#toString` avoids issues with the `typeof` operator
  49. // in Safari 8 which returns 'object' for typed array constructors, and
  50. // PhantomJS 1.9 which returns 'function' for `NodeList` instances.
  51. var tag = isObject( value ) ? Object.prototype.toString.call( value ) : '';
  52. return tag == '[object Function]' || tag == '[object GeneratorFunction]';
  53. }
  54. function isObject( value ) {
  55. // Avoid a V8 JIT bug in Chrome 19-20.
  56. // See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
  57. var type = typeof value;
  58. return !! value && ( type == 'object' || type == 'function' );
  59. }
  60. function compareArrays( val1, val2 ) {
  61. var isArr1 = Array.isArray( val1 );
  62. var isArr2 = Array.isArray( val2 );
  63. // Compare type.
  64. if ( isArr1 !== isArr2 ) return makeFail( 'Values are not both arrays' );
  65. // Not arrays. Continue.
  66. if ( ! isArr1 ) return undefined;
  67. // Compare length.
  68. var N1 = val1.length;
  69. var N2 = val2.length;
  70. if ( N1 !== val2.length ) return makeFail( 'Array length differs', N1, N2 );
  71. // Compare content at each index.
  72. for ( var i = 0; i < N1; i ++ ) {
  73. var cmp = areEqual( val1[ i ], val2[ i ] );
  74. if ( ! cmp ) return addContext( 'array index "' + i + '"' );
  75. }
  76. // Arrays are equal.
  77. return true;
  78. }
  79. function compareObjects( val1, val2 ) {
  80. var isObj1 = isObject( val1 );
  81. var isObj2 = isObject( val2 );
  82. // Compare type.
  83. if ( isObj1 !== isObj2 ) return makeFail( 'Values are not both objects' );
  84. // Not objects. Continue.
  85. if ( ! isObj1 ) return undefined;
  86. // Compare keys.
  87. var keys1 = Object.keys( val1 );
  88. var keys2 = Object.keys( val2 );
  89. for ( var i = 0, l = keys1.length; i < l; i ++ ) {
  90. if ( keys2.indexOf( keys1[ i ] ) < 0 ) {
  91. return makeFail( 'Property "' + keys1[ i ] + '" is unexpected.' );
  92. }
  93. }
  94. for ( var i = 0, l = keys2.length; i < l; i ++ ) {
  95. if ( keys1.indexOf( keys2[ i ] ) < 0 ) {
  96. return makeFail( 'Property "' + keys2[ i ] + '" is missing.' );
  97. }
  98. }
  99. // Keys are the same. For each key, compare content until a difference is found.
  100. var hadDifference = false;
  101. for ( var i = 0, l = keys1.length; i < l; i ++ ) {
  102. var key = keys1[ i ];
  103. if ( key === "uuid" || key === "id" ) {
  104. continue;
  105. }
  106. var prop1 = val1[ key ];
  107. var prop2 = val2[ key ];
  108. // Compare property content.
  109. var eq = areEqual( prop1, prop2 );
  110. // In case of failure, an message should already be set.
  111. // Add context to low level message.
  112. if ( ! eq ) {
  113. addContext( 'property "' + key + '"' );
  114. hadDifference = true;
  115. }
  116. }
  117. return ! hadDifference;
  118. }
  119. function makeFail( msg, val1, val2 ) {
  120. message = msg;
  121. if ( arguments.length > 1 ) message += " (" + val1 + " vs " + val2 + ")";
  122. return false;
  123. }
  124. function addContext( msg ) {
  125. // There should already be a validation message. Add more context to it.
  126. message = message || "Error";
  127. message += ", at " + msg;
  128. return false;
  129. }
  130. }
  131. export { SmartComparer };