UIZE JavaScript Framework

MODULES Uize.Data.Diff

1. Introduction

The Uize.Data.Diff module provides a way to compare all the leaf node properties of two data objects and report the property differences in the form of a diff result object.

DEVELOPERS: Chris van Rensburg

1.1. In a Nutshell

The Uize.Data.Diff module makes it easy to compare two objects to determine how they differ.

The Uize.Data.Diff.diff method accepts two objects are arguments, along with an optional property comparer function, and then compares all the leaf node properties of those two objects and returns a diff result.

1.1.1. Diff Result

When the Uize.Data.Diff.diff method is used to compare two objects, it produces a diff result.

The diff result is an object whose structure is based on a union of the structures of the two objects being compared. Depending on the property comparer function that is used when performing the diff, the diff result can be a complete or sparse union of the structures of the objects being compared - it is not required that the diff result contain a property for every property that is compared.

The diff result can also be purely descriptive of the differences in properties between the objects being compared (e.g. 'modified', 'added', 'removed', 'unchanged'), or the diff result can reflect actual values from the properties (values for only the properties that are the same, values for only the properties that are different, etc.).

The diff result object is best explained with an example...

EXAMPLE

Uize.Data.Diff.diff (
  // object 1
  {
    foo:'fooValue',
    bar:'barValue',
    qux:'quxValue'
  },

  // object 2
  {
    foo:'FOO_VALUE',
    baz:'bazValue',
    qux:'quxValue'
  }
);

RESULT

{
  foo:'modified',
  bar:'removed',
  baz:'added',
  qux:'unchanged'
}

In the diff result object, you will notice that there are properties for all of the combined properties of both of the objects being compared. This is due to the behavior of the default property comparer, which sets the values of the properties in the diff result object as descriptions of how the properties differ between the objects being compared - there are values in the diff result for properties that have been added or removed (i.e. they don't exist in both objects).

1.1.2. Diffing is Recursive

When two objects are compared using the Uize.Data.Diff.diff method, the objects are compared recursively so that all leaf nodes are compared.

EXAMPLE

Uize.Data.Diff.diff (
  // object 1
  {
    foo:{
      bar:'barValue'
    },
    baz:{
      qux:'quxValue',
      hello:'world'
    }
  },

  // object 2
  {
    foo:{
      bar:'BAR_VALUE'
    },
    baz:{
      hello:'world',
      more:'stuff'
    }
  }
);

RESULT

{
  foo:{
    bar:'modified'
  },
  baz:{
    qux:'removed',
    hello:'unchanged',
    more:'added'
  }
}

1.1.3. Diffing Asymmetrical Objects

When two objects are compared using the Uize.Data.Diff.diff method, it is the union of the two objects that is compared recursively, so that all leaf nodes of both objects are compared.

EXAMPLE

Uize.Data.Diff.diff (
  // object 1
  {
    foo:{
      bar:'barValue',
      baz:'bazValue'
    },
    qux:'quxValue'
  },

  // object 2
  {
    bar:{
      foo:'fooValue',
      baz:'bazValue'
    },
    qux:'quxValue'
  }
);

RESULT

{
  foo:{
    bar:'removed',
    baz:'removed'
  },
  qux:'unchanged',
  bar:{
    foo:'added',
    baz:'added'
  }
}

1.1.4. How Diffing is Performed

Comparing two objects using the Uize.Data.Diff.diff method involves the following process...

The method itereates recursively over the union of the two objects being compared.
For each leaf node property, the method calls a property comparer function in order to compare the values of the property between the two objects.
The property comparer function is passed two property profile objects as arguments. These property profile objects describe the values of the property for each of the two objects being compared.
The property comparer function is expected to return a property profile object that describes the property that should be placed into the corresponding spot in the diff result object.

1.1.5. Property Comparer

The Uize.Data.Diff.diff method uses a property comparer function to compare the values of the leaf node property between the two objects being compared.

1.1.5.1. Property Comparer Function

A property comparer function should accept up to two property profile arguments, and should return a property profile as its result.

Consider the following example of a property comparer function...

EXAMPLE

function (obj1PropProfile,obj2PropProfile) {
  return obj1PropProfile && !obj2PropProfile ? {value:'removed'} : undefined;
}

The property comparer function in the above example can be used with the Uize.Data.Diff.diff method to find only the properties that exist in the first object (obj1PropProfile) but that don't exist in the second object (obj2PropProfile).

The function checks to see if the property profile for the property in the first object is truthy and falsy in the second object. If so, it returns a property profile with 'removed' for the value property, which would result in the value 'removed' being set for the corresponding property in the diff result object. Otherwise, it returns the value undefined, which would result in no property being added to the diff result object.

So, this property comparer function can be used to find just the properties that have been "removed" between the first object and the second object. If the returned diff result object is empty, then no properties have neen removed. If the diff result object is not empty, then its contents will indicate which properties have been removed.

1.1.5.1.1. Property Profile

In the context of a property comparer function, a property profile is either...

an object, providing information on the name and value of the property
the value undefined, indicating that the property doesn't exist for one of the objects being compared, or shouldn't exist in the diff result object

When the property profile is an object value, it will be of the form...

{
  key:keySTR,     // the name of the property
  value:valueSTR  // the value of the property
}
1.1.5.1.2. Returning a Property Profile

There are a few things to note about the property profile that is returned by the property comparer function...

If the value undefined is returned in place of a property profile object, then no property will be added to the diff result object for the property being compared.
You may return one of the property profile arguments, so a check can be used to determine if the value for property from the first object should be used or if the value for the property from the second object should be used for the value of the property in the diff result.
If a key property is present in the returned property profile object, its value will determine the name of the property in the diff result object - this makes it possible to perform key re-mapping / renaming when comparing two objects (see the example Rename Keys for Leaf Nodes of an Object).
When returning a new object, the key property may be omitted, in which case the property name will be the name of the property being compared and will not be re-mapped.

1.1.5.2. Default Property Comparer

When no value is specified for the optional propertyComparerFUNC third argument of the Uize.Data.Diff.diff method, the default property comparer function will be used.

The default property comparer function produces a comparison result value for every property being compared. Each property in the diff result object can have one of the following possible values...

'unchanged' - the property exists in both of the objects being compared and the values are identical
'modified' - the property exists in both of the objects being compared, but the values differ
'added' - the property doesn't exist in the first object but does exist in the second object
'removed' - the property exists in the first object but does not exist in the second object

The default property comparer is implemented as follows...

function (obj1PropProfile,obj2PropProfile) {
  return {
    value:obj1PropProfile && !obj2PropProfile
      ? 'removed'
      : !obj1PropProfile && obj2PropProfile
        ? 'added'
        : obj1PropProfile.value === obj2PropProfile.value
          ? 'unchanged'
          : 'modified'
  };
}

1.1.5.3. Examples of Property Comparer Functions

The Uize.Data.Diff.diff method is extremely versatile and can be used with different types of property comparer functions to achieve a wide variety of different effects.

1.1.5.3.1. Find Added or Modified Values

In order to obtain the values for all properties that have been added or modified in the second object, the following property comparer function can be used...

function (obj1PropProfile,obj2PropProfile) {
  return (
    obj2PropProfile && (!obj1PropProfile || obj2PropProfile.value !== obj1PropProfile.value)
      ? obj2PropProfile
      : undefined
  );
}
1.1.5.3.2. Get a Summary of Just the Differences

In order to obtain a summary of just the differences between two objects, the following property comparer function can be used...

function (obj1PropProfile,obj2PropProfile) {
  return {
    obj1PropProfile && !obj2PropProfile
      ? {value:'removed'}
      : !obj1PropProfile && obj2PropProfile
        ? {value:'added'}
        : obj1PropProfile.value === obj2PropProfile.value
          ? undefined
          : {value:'modified'}
  };
}

If the above property compater function is used when comparing two objects and the object returned by the Uize.Data.Diff.diff method is empty, then the objects being compared can be considered identical.

1.1.5.3.3. Get a Summary of Structural Differences

In order to obtain a summary of just the structural differences between two objects, the following property comparer function can be used...

function (obj1PropProfile,obj2PropProfile) {
  return {
    obj1PropProfile && !obj2PropProfile
      ? {value:'removed'}
      : !obj1PropProfile && obj2PropProfile
        ? {value:'added'}
        : undefined
  };
}

If the above property compater function is used when comparing two objects and the object returned by the Uize.Data.Diff.diff method is empty, then the objects being compared can be considered to have identical structure - even if the values of the properties may differ between the two objects.

1.1.5.3.4. Get a Summary of Structural and Type Differences

In order to obtain a summary of just the structural and type differences between two objects, the following property comparer function can be used...

function (obj1PropProfile,obj2PropProfile) {
  return {
    obj1PropProfile && !obj2PropProfile
      ? {value:'removed'}
      : !obj1PropProfile && obj2PropProfile
        ? {value:'added'}
        : typeof obj1PropProfile.value !== typeof obj2PropProfile.value
          ? {value:'type mismatch'}
          : undefined
  };
}

If the above property compater function is used when comparing two objects and the object returned by the Uize.Data.Diff.diff method is empty, then the objects being compared can be considered to have identical structure and type. This can be a useful way of testing if an object conforms to the structure and type requirements of some reference object.

1.1.5.3.5. Get the Intersection Between Two Objects

In order to obtain the values for all properties that are identical in the objects being compared, the following property comparer function can be used...

function (obj1PropProfile,obj2PropProfile) {
  return (
    obj1PropProfile && obj2PropProfile && obj1PropProfile.value === obj2PropProfile.value
      ? obj1PropProfile
      : undefined
  );
}
1.1.5.3.6. Perform a Conditional Merge

In order to conditionally merge all properties of a second object with all properties of a first object, so that a property from the second object is only merged in if it doesn't exist in the first object, the following property comparer function can be used...

function (obj1PropProfile,obj2PropProfile) {
  return obj1PropProfile || obj2PropProfile;
}

EXAMPLE

Uize.Data.Diff.diff (
  {
    foo:'foo',
    bar:{
      baz:'baz'
    }
  },
  {
    foo:'FOO',
    bar:{
      baz:'BAZ',
      qux:'QUX'
    }
  },
  function (obj1PropProfile,obj2PropProfile) {
    return obj1PropProfile || obj2PropProfile;
  }
);

RESULT

{
  foo:'foo',
  bar:{
    baz:'baz',
    qux:'QUX'
  }
}
1.1.5.3.7. Create an Initialized Clone of an Object

The Uize.Data.Diff.diff method can be used in a less conventional way to create an initialized clone of a source object using the following approach...

EXAMPLE

Uize.Data.Diff.diff (
  {
    foo:'foo',
    bar:{baz:{qux:'qux'}}
  },
  {},
  function () {return {value:''}}
);

RESULT

{
  foo:'',
  bar:{baz:{qux:''}}
}
1.1.5.3.8. Iterate Recursively Over the Leaf Nodes of an Object

The Uize.Data.Diff.diff method can be used to recursively iterate over all the leaf nodes of an object.

EXAMPLE

Uize.Data.Diff.diff (
  sourceObj,  // the object you wish to iterate over
  {},         // specify an empty object for the second object
  function (propertyProfile) {
    // do something for each leaf node
  }
);

When using the Uize.Data.Diff.diff method as essentially an object iterator, the first argument can be used for specifying the object you wish to iterate over, and an empty object can be specified for the second object.

Then, in the property comparer function that you specify, the information about the leaf node property currently being iterated over can be accessed from the first property profile argument, and the second argument can be ignored.

1.1.5.3.9. Map Values for Leaf Nodes of an Object

The Uize.Data.Diff.diff method can be used to map values for all the leaf nodes of an object.

EXAMPLE

var remappedValuesObj = Uize.Data.Diff.diff (
  sourceObj,  // the object you wish to iterate over
  {},         // specify an empty object for the second object
  function (propertyProfile) {
    // modify the value property of the propertyProfile argument
    return propertyProfile;
  }
);

When using the Uize.Data.Diff.diff method as a value mapper for object properties, the first argument can be used for specifying the object you wish to map, and an empty object can be specified for the second object.

Then, in the property comparer function that you specify, the information about the leaf node property currently being iterated over can be accessed from the first property profile argument, and the second argument can be ignored. In order to map the value for a property, simply modify the value property of the property profile object and return this modified object as the result.

EXAMPLE

Uize.Data.Diff.diff (
  {
    foo:'foo',
    bar:{baz:{qux:'qux'}}
  },
  {},
  function (propertyProfile) {
    propertyProfile.value = propertyProfile.value.toUpperCase ();
    return propertyProfile;
  }
);

RESULT

{
  foo:'FOO',
  bar:{baz:{qux:'QUX'}}
}
1.1.5.3.10. Rename Keys for Leaf Nodes of an Object

The Uize.Data.Diff.diff method can be used to rename keys for all the leaf nodes of an object.

EXAMPLE

var renamedKeysObj = Uize.Data.Diff.diff (
  sourceObj,  // the object you wish to iterate over
  {},         // specify an empty object for the second object
  function (propertyProfile) {
    // modify the key property of the propertyProfile argument
    return propertyProfile;
  }
);

When using the Uize.Data.Diff.diff method as a key renamer for object leaf node properties, the first argument can be used for specifying the source object, and an empty object can be specified for the second object.

Then, in the property comparer function that you specify, the information about the leaf node property currently being iterated over can be accessed from the first property profile argument, and the second argument can be ignored. In order to rename the key for a property, simply modify the key property of the property profile object and return this modified object as the result.

EXAMPLE

Uize.Data.Diff.diff (
  {
    foo:'foo',
    bar:{baz:{qux:'qux'}}
  },
  {},
  function (propertyProfile) {
    propertyProfile.key = propertyProfile.key.toUpperCase ();
    return propertyProfile;
  }
);

RESULT

{
  FOO:'foo',
  bar:{baz:{QUX:'qux'}}
}

Notice from the above example that not all keys have been renamed - only the keys for the leaf node properties. This is because the property comparer function is only called for leaf node properties.

1.2. Examples

There are no dedicated showcase example pages for the Uize.Data.Diff module.

SEARCH FOR EXAMPLES

Use the link below to search for example pages on the UIZE Web site that reference the Uize.Data.Diff module...

SEARCH

1.3. Implementation Info

The Uize.Data.Diff module defines the Uize.Data.Diff package under the Uize.Data namespace.

1.3.1. Features Introduced in This Module

The features listed in this section have been introduced in this module.

STATIC METHODS

Uize.Data.Diff.diff

STATIC PROPERTIES

Uize.Data.Diff.moduleName | Uize.Data.Diff.pathToResources

1.3.2. Features Overridden in This Module

No features have been overridden in this module.

1.3.3. Features Inherited From Other Modules

This module has no inherited features.

1.3.4. Modules Directly Under This Namespace

There are no modules directly under this namespace.

1.3.5. Unit Tests

The Uize.Data.Diff module is unit tested by the Uize.Test.Uize.Data.Diff test module.

2. Static Methods

2.1. Uize.Data.Diff.diff

Performs a diff between two objects, comparing all the corresponding leaf node properties, and reports the difference in the form of a diff result object.

DIFFERENT USAGES

Diff Two Objects, Using the Default Property Comparer

diffResultOBJ = Uize.Data.Diff.diff (object1OBJ,object2OBJ);

Diff Two Objects, Using a Custom Property Comparer

diffResultOBJ = Uize.Data.Diff.diff (object1OBJ,object2OBJ,propertyComparerFUNC);

2.1.1. Diff Two Objects, Using the Default Property Comparer

In its most basic usage, two objects can be compared using the default property comparer by specifying just the two objects to be compared as arguments.

SYNTAX

diffResultOBJ = Uize.Data.Diff.diff (object1OBJ,object2OBJ);

When the default property comparer is used, the result returned by this method is an object that represents the union between the two objects being compared, and where the value of each leaf node describes the difference, for the corresponding property, between the two objects.

EXAMPLE

Uize.Data.Diff.diff (
  {
    foo:'foo',
    bar:'bar',
    baz:'baz'
  },
  {
    foo:'foo',
    bar:'BAR',
    qux:'qux'
  }
);

RESULT

{
  foo:'unchanged',
  bar:'modified',
  baz:'removed',
  qux:'added'
}

For a more in-depth discussion, see the section on the default property comparer.

2.1.2. Diff Two Objects, Using a Custom Property Comparer

In cases where the behavior of the default property comparer is not suitable, a custom property comparer function can be specified for the optional third argument.

SYNTAX

diffResultOBJ = Uize.Data.Diff.diff (object1OBJ,object2OBJ,propertyComparerFUNC);

EXAMPLE

Uize.Data.Diff.diff (
  {
    foo:{
      bar:'bar'
    }
  },
  {
    foo:{
      bar:'bar',
      baz:'baz'
    },
    qux:'qux'
  },
  function (obj1PropProfile,obj2PropProfile) {
    return obj1PropProfile ? undefined : obj2PropProfile;
  }
);

In the above example, the Uize.Data.Diff.diff method is being used with a custom property comparer function to return an object that contains the values of only the properties that are "added" in the second object - in other words, properties that exist in the second object that don't exist in the first object.

The implementation of this custom property comparer function is quite straightforward: the function returns undefined if a property exists in the first object - otherwise, it returns the profile for the property from the second object. When the function returns undefined, no property is added to the resulting diff result object. But, when the function returns the profile for the property from the second object, then the value of the property in the second object is added to the diff result object.

This means that the diff result object will only contain leaf node properties for properties that exist in the second object and that do not exist in the first, and the value for the properties in the diff result object will be the values of the "added" properties in the second object. From our above example we would, therefore, obtain the following result...

RESULT

{
  foo:{
    baz:'baz'
  },
  qux:'qux'
}

2.1.3. Diff Two Objects, Specifying Additional Diffing Options

In cases where the standard behavior of this method is not suitable, its behavior can be customized by specifying values for options through the optional optionsOBJ fourth argument.

SYNTAX

diffResultOBJ = Uize.Data.Diff.diff (object1OBJ,object2OBJ,propertyComparerFUNC,optionsOBJ);

Because the options are specified through the fourth argument, this means that the propertyComparerFUNC third argument must necessarily be specified in this usage. Therefore, if you wish to use the default property comparer with this usage you should specify the value null or undefined for the propertyComparerFUNC argument.

2.1.3.1. optionsOBJ

The value specified for the optionsOBJ argument should be an object of the form...

OPTIONS

{
  skeleton:skeletonBOOL  // whether or not a skeleton should be returned at a minimum
}
2.1.3.1.1. skeleton

A boolean, specifying whether or not a structural skeleton will be returned at a minimum when two objects are compared that either have empty nodes or where the property comparer function returns undefined for all compared properties.

When the value true is specified for the skeleton option, then the returned comparison result will at a minimum be a skeleton object that is the union of the structures of the two objects being compared.
When the value false is specified for the skeleton option, then the returned comparison result will not contain empty nodes in which there are no property comparison values.
When no value is specified for the skeleton option, or if the optionsOBJ argument is omitted, then the value for the skeleton option is defaulted to false.

EXAMPLE

Uize.Data.Diff.diff (
  {
    address:{
      mailing:{
        line1:'123 Strange Blvd',
        line2:'Apt 6',
        city:'Strangeville',
        state:'CA',
        zip:'12345'
      }
    },
    contact:{
      phoneNumbers:{
        home:'(555) 123-4567',
        cell:'(555) 555-5555',
        work:'(555) 999-9999'
      }
    }
  },
  {
    address:{
      shipping:{
        line1:'456 Work Plaze',
        line2:'Suite 400',
        city:'Workville',
        state:'CA',
        zip:'67890'
      }
    },
    contact:{
      other:{
        email:'[email protected]',
        IRC:'mstrange'
      }
    }
  },
  function () {return undefined},
  {skeleton:true}
);

In the above example, two objects are being compared that have similar but slightly differing structures. The custom property comparer function that is being specified always returns the value undefined. Normally, this would mean that the comparison result object would be empty. But, because the value true is being specified for the skeleton option, the method returns a skeleton object that is the union of the structures of the two objects being compared, as shown below...

RESULT

{
  address:{
    mailing:{},
    shipping:{}
  },
  contact:{
    phoneNumbers:{},
    other:{}
  }
}

IMPLEMENTATION INFO

this feature was introduced in this module

3. Static Properties

3.1. Uize.Data.Diff.moduleName

IMPLEMENTATION INFO

this feature was introduced in this module

3.2. Uize.Data.Diff.pathToResources

IMPLEMENTATION INFO

this feature was introduced in this module