deep-copy-diagnostics

1.0.2 • Public • Published

Introduction to deep-copy-diagnostics 1.0.2

Stop using deep-strict-equal algorithms to verify deep copies because they don't detect deep equivalency. Deep-equivalency, not deep-strict-equality, describes deep-copies. This package's deepEquivalent() function should be your tool for testing deep copies and debugging deep copying algorithms.

The reason you want this package is for its correctness, as proven by extensive testing, its speed, and its concise reporting of why deep-equivalency fails when it does. deepEquivalent(), has no peers on NPM as far as I can tell. deepEquivalent() is 3-5 times faster than some deep-strict-equal algorithms, including assert.deepStrictEqual(), which don't have as much work to do, while the deep-strict-equal algorithms that go faster tend to be very incorrect, having cut too many corners.

deepEquivalent(x,y) catches every possible way in which x and y may not be deep copies of each other. It takes into account internal prototypes, circular references, duplicate references, functions, getters/setters, property descriptors, and frozen/sealed/extensible states.

Some deep copiers on NPM will copy functions, Booleans, etc., as is. deepEquivalent(x,y) can handle that: see Usage below.

Usage

npm install deep-equal-diagnostics
const {deepEquivalent} = require('deep-copy-diagnostics');

if(deepEquivalent(x,y))
{
    // statement that x and y are deeply equivalent
    // (that x and y are deep copies of each other)
    
    console.log(deepEquivalent.message); 
    
    // x and y are deep copies of each other.
    // By the way, corresponding functions must not be 
    // reference equal because deep copies don't share
    // state. But read below to see how to relax this
    // requirement.
}
else
{
    // Very clear and precise reason why x and y are 
    // not deeply equivalent.
    
    console.log(deepEquivalent.message); 
    
    // Examine the two subobjects that were
    // not equivalent if you want.
    
    const {source, target} = deepEquivalent.pair;    
}

if(deepEquivalent(x,y, new Set([Function, Boolean])
{
    console.log(deepEquivalent.message); 
    
    // statement that x and y are deeply equivalent
    // except that corresponding functions are expected to
    // be reference-equal, and corresponding Booleans are 
    // expected to be reference-equal. 
}
else
{
     // Very clear and precise reason why x and y are not 
     // deeply equivalent, with above noted exceptions 
     // enforced.
    
    console.log(deepEquivalent.message);  
    
    // Examine the two subobjects that were
    // not equivalent or the two subobjects that 
    // were expected to be reference-equal but 
    // were not.
    
    const {source, target} = deepEquivalent.pair;       
}

Exports

export description
deepEquivalent(x,y, primitives) tests for deep equivalence. primitives is a set of classes whose instances are to be reference equal: see Usage

Testing is Extensive

See the Tests folder.

Deep-Strict-Equal Algorithms are Bad for Detecting Deep Copies

Two objects can pass the deep-strict-equal test yet not be deep copies of each other. This can happen in at least two ways as demonstrated below.

Example 1
    const A = new Date();
    const x = {a:A};
    const y = {a:A}
    assert.deepStrictEqual(x,y); // Says all is good 
    // No exception thrown.
    
    x and y are not deep copies because they share the 'a' 
    property, even though it passes the deepStrictEqual
    test.
    
Example 2
    const x = {a:{b:{c:1}}}
    const y = {a:{b:{c:1}}}
    x.a.b.d = x
    y.a.b.d = x     
    assert.deepStrictEqual(A,B); // Says all is good 
    // No exception thrown.
    
    x and y are not deep copies because they have a 
    circular reference in different parts of the object 
    trees, even though it passes the deepStrictEqual
    test.       

Definition of Deep Equivalence

Read this section only if you want.

The elements x and y are deeply equivalent if the elements in their trees are in a one-one correspondence that meets the following conditions.

  1. x corresponds to y
  2. Corresponding elements must have the same type (*).
  3. Corresponding objects must have the same internal prototype.
  4. Corresponding primitives must have the same value.
  5. Corresponding WeakMaps or WeakSets must be reference equal.
  6. Corresponding Booleans, Numbers, Strings, Dates, RegExps must have the same internal state, i.e., valueOfs() or toStrings() must be the same.
  7. Corresponding Functions as strings must be the same.
  8. Corresponding objects are never reference equal (except for WeakSets and WeakMaps).
  9. Corresponding objects must have the same frozen/sealed/extensible states.
  10. If p and q are corresponding objects
    1. if either p.a or q.a exist then both exist, and they correspond. Moreover, the property descriptors must be the same.
    2. if p is a Set (we know by 2 that q is a Set) then insertion order defines the one-one correspondence between their members.
    3. if p is a Map (we know by 2 that q is a Map) then insertion order defines the one-one correspondences between their keys and their values.

It would be easy for deepEquivalent() to use the theoretical definition that says insertion order does not matter for Sets and Maps. However, it would be perverse for a deep copier to jumble the insertion order up. For example, when copying a Set, a deep copier will read its members in insertion order, and then copy them in insertion order.

(*) As determined by the dtype(x) function of the type-quickly package.

Version History

Version Published
1.0.0 4-27-2022 deep copy diagnostic testing
1.0.1 4-28-2022 corrected typo $path[mapKey[${n}]] --> ${path}[mapKey[${n}]]
1.0.2 5-4-2022 Fixed: did not examine buffer properties of Typed Arrays for circularity/duplicity. New tests added to show its now correct.

Package Sidebar

Install

npm i deep-copy-diagnostics

Weekly Downloads

0

Version

1.0.2

License

ISC

Unpacked Size

93.4 kB

Total Files

17

Last publish

Collaborators

  • markw9