SmartComparer.js 4.5 KB

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