superdiff

Superdiff compares two arrays or objects and returns a full diff of their differences in a readable format.
GitHub
675
Created 2 years ago, last commit 5 hours ago
2 contributors
54 commits
Stars added on GitHub, per day, on average
Yesterday
+2
Last week
+11.0
/day
Package on NPM
Monthly downloads on NPM
10
11
12
1
2
3
4
5
6
7
8
9
2023
2024
No dependencies
README

superdiff-logo

CI CD NPM Downloads GitHub Tag


WHAT IS IT?

This library compares two arrays or objects and returns a full diff of their differences.


WHY YOU SHOULD USE THIS LIBRARY

Most existing solutions return a confusing diff format that often requires extra parsing. They are also limited to object comparison.

Superdiff provides a complete and readable diff for both arrays and objects. Plus, it's battle-tested, has zero dependencies, and is super fast.

Import. Enjoy. 👍


DONORS

I am grateful to the generous donors of Superdiff!

AlexisAnzieu omonk sneko


FEATURES

Superdiff exports 5 functions:

// Returns a complete diff of two objects
getObjectDiff(prevObject, nextObject)

// Returns a complete diff of two arrays
getListDiff(prevList, nextList)

// Streams the diff of two object lists, ideal for large lists and maximum performance
streamListDiff(prevList, nextList, referenceProperty)

// Checks whether two values are equal 
isEqual(dataA, dataB)

// Checks whether a value is an object
isObject(data)

getObjectDiff()

import { getObjectDiff } from "@donedeal0/superdiff";

Compares two objects and returns a diff for each value and its possible subvalues. Supports deeply nested objects of any value type.

FORMAT

Input

prevData: Record<string, unknown>;
nextData: Record<string, unknown>;
options?: {
  ignoreArrayOrder?: boolean, // false by default,
  showOnly?: {
    statuses: ("added" | "deleted" | "updated" | "equal")[], // [] by default
    granularity?: "basic" | "deep" // "basic" by default
  }
}
  • prevData: the original object.
  • nextData: the new object.
  • options
    • ignoreArrayOrder: if set to true, ["hello", "world"] and ["world", "hello"] will be treated as equal, because the two arrays contain the same values, just in a different order.

    • showOnly: returns only the values whose status you are interested in. It takes two parameters:

      • statuses: status you want to see in the output (e.g. ["added", "equal"])
        • granularity:
          • basic returns only the main properties whose status matches your query.
          • deep can return main properties if some of their subproperties' status match your request. The subproperties are filtered accordingly.

Output

type ObjectDiff = {
  type: "object";
  status: "added" | "deleted" | "equal" | "updated";
  diff: Diff[];
};

/** recursive diff in case of subproperties */
type Diff = {
  property: string;
  previousValue: unknown;
  currentValue: unknown;
  status: "added" | "deleted" | "equal" | "updated";
  diff?: Diff[];
};

USAGE

Input

getObjectDiff(
  {
    id: 54,
    user: {
      name: "joe",
-     member: true,
-     hobbies: ["golf", "football"],
      age: 66,
    },
  },
  {
    id: 54,
    user: {
      name: "joe",
+     member: false,
+     hobbies: ["golf", "chess"],
      age: 66,
    },
  }
);

Output

{
      type: "object",
+     status: "updated",
      diff: [
        {
          property: "id",
          previousValue: 54,
          currentValue: 54,
          status: "equal",
        },
        {
          property: "user",
          previousValue: {
            name: "joe",
            member: true,
            hobbies: ["golf", "football"],
            age: 66,
          },
          currentValue: {
            name: "joe",
            member: false,
            hobbies: ["golf", "chess"],
            age: 66,
          },
+         status: "updated",
          diff: [
            {
              property: "name",
              previousValue: "joe",
              currentValue: "joe",
              status: "equal",
            },
+           {
+             property: "member",
+             previousValue: true,
+             currentValue: false,
+             status: "updated",
+           },
+           {
+             property: "hobbies",
+             previousValue: ["golf", "football"],
+             currentValue: ["golf", "chess"],
+             status: "updated",
+           },
            {
              property: "age",
              previousValue: 66,
              currentValue: 66,
              status: "equal",
            },
          ],
        },
      ],
    }

getListDiff()

import { getListDiff } from "@donedeal0/superdiff";

Compares two arrays and returns a diff for each entry. Supports duplicate values, primitive values and objects.

FORMAT

Input

  prevList: T[];
  nextList: T[];
  options?: {
    showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
    referenceProperty?: string, // "" by default
    ignoreArrayOrder?: boolean, // false by default,
    considerMoveAsUpdate?: boolean // false by default
  }
  • prevList: the original list.
  • nextList: the new list.
  • options
    • showOnly gives you the option to return only the values whose status you are interested in (e.g. ["added", "equal"]).
    • referenceProperty will consider an object to be updated rather than added or deleted if one of its properties remains stable, such as its id. This option has no effect on other datatypes.
    • ignoreArrayOrder: if set to true, ["hello", "world"] and ["world", "hello"] will be treated as equal, because the two arrays contain the same values, just in a different order.
    • considerMoveAsUpdate: if set to true a moved value will be considered as updated.

Output

type ListDiff = {
  type: "list";
  status: "added" | "deleted" | "equal" | "moved" | "updated";
  diff: {
    value: unknown;
    prevIndex: number | null;
    newIndex: number | null;
    indexDiff: number | null;
    status: "added" | "deleted" | "equal" | "moved" | "updated";
  }[];
};

USAGE

Input

getListDiff(
- ["mbappe", "mendes", "verratti", "ruiz"],
+ ["mbappe", "messi", "ruiz"]
);

Output

{
      type: "list",
+     status: "updated",
      diff: [
        {
          value: "mbappe",
          prevIndex: 0,
          newIndex: 0,
          indexDiff: 0,
          status: "equal",
        },
-       {
-         value: "mendes",
-         prevIndex: 1,
-         newIndex: null,
-         indexDiff: null,
-         status: "deleted",
-       },
-       {
-         value: "verratti",
-         prevIndex: 2,
-         newIndex: null,
-         indexDiff: null,
-         status: "deleted",
-       },
+       {
+         value: "messi",
+         prevIndex: null,
+         newIndex: 1,
+         indexDiff: null,
+         status: "added",
+       },
+       {
+         value: "ruiz",
+         prevIndex: 3,
+         newIndex: 2,
+         indexDiff: -1,
+         status: "moved",
        },
      ],
    }

streamListDiff()

import { streamListDiff } from "@donedeal0/superdiff";

Streams the diff of two object lists, ideal for large lists and maximum performance.

FORMAT

Input

 prevList: Record<string, unknown>[],
 nextList: Record<string, unknown>[],
 referenceProperty: keyof Record<string, unknown>,
 options: {
  showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
  chunksSize?: number, // // 0 by default
  considerMoveAsUpdate? boolean; // false by default
}
  • prevList: the original object list.
  • nextList: the new object list.
  • referenceProperty: a property common to all objects in your lists (e.g. id).
  • options
    • chunksSize the number of object diffs returned by each streamed chunk. (e.g. 0 = 1 object diff per chunk, 10 = 10 object diffs per chunk).
    • showOnly gives you the option to return only the values whose status you are interested in (e.g. ["added", "equal"]).
    • considerMoveAsUpdate: if set to true a moved value will be considered as updated.

Output

The objects diff are grouped into arrays - called chunks - and are consumed thanks to an event listener. You have access to 3 events:

  • data: to be notified when a new chunk of object diffs is available.
  • finish: to be notified when the stream is finished.
  • error: to be notified if an error occurs during the stream.
interface StreamListener<T extends Record<string, unknown>> {
  on<E extends keyof EmitterEvents<T>>(
    event: E,
    listener: Listener<EmitterEvents<T>[E]>,
  ): this;
}

type EmitterEvents<T extends Record<string, unknown>> = {
  data: [StreamListDiff<T>[]];
  error: [Error];
  finish: [];
};


type StreamListDiff<T extends Record<string, unknown>> = {
  currentValue: T | null;
  previousValue: T | null;
  prevIndex: number | null;
  newIndex: number | null;
  indexDiff: number | null;
  status: "added" | "deleted" | "moved" | "updated" | "equal";
};

USAGE

Input

const diff = streamListDiff(
      [ 
-       { id: 1, name: "Item 1" },  
        { id: 2, name: "Item 2" },
        { id: 3, name: "Item 3" } 
      ],
      [
+       { id: 0, name: "Item 0" }, 
        { id: 2, name: "Item 2" },
+       { id: 3, name: "Item Three" },
      ],
      "id", 
      { chunksSize: 2 }
    );

Output

diff.on("data", (chunk) => {
      // first chunk received (2 object diffs)
      [
+       {
+         previousValue: null,
+         currentValue: { id: 0, name: 'Item 0' },
+         prevIndex: null,
+         newIndex: 0,
+         indexDiff: null,
+         status: 'added'
+       },
-       {
-         previousValue: { id: 1, name: 'Item 1' },
-         currentValue: null,
-         prevIndex: 0,
-         newIndex: null,
-         indexDiff: null,
-         status: 'deleted'
-       }
      ]
    // second chunk received (2 object diffs)
      [
        {
          previousValue: { id: 2, name: 'Item 2' },
          currentValue: { id: 2, name: 'Item 2' },
          prevIndex: 1,
          newIndex: 1,
          indexDiff: 0,
          status: 'equal'
        },
+       {
+         previousValue: { id: 3, name: 'Item 3' },
+         currentValue: { id: 3, name: 'Item Three' },
+         prevIndex: 2,
+         newIndex: 2,
+         indexDiff: 0,
+         status: 'updated'
+       },
     ]
});

diff.on("finish", () => console.log("The full diff is available"))
diff.on("error", (err) => console.log(err))

isEqual()

import { isEqual } from "@donedeal0/superdiff";

Tests whether two values are equal.

FORMAT

Input

a: unknown,
b: unknown,
options: { 
    ignoreArrayOrder: boolean; // false by default
     },
  • a: the value to be compared to the value b.
  • b: the value to be compared to the value a.
  • ignoreArrayOrder: if set to true, ["hello", "world"] and ["world", "hello"] will be treated as equal, because the two arrays contain the same values, just in a different order.

USAGE

isEqual(
  [
    { name: "joe", age: 99 },
    { name: "nina", age: 23 },
  ],
  [
    { name: "joe", age: 98 },
    { name: "nina", age: 23 },
  ],
);

Output

false;

isObject()

import { isObject } from "@donedeal0/superdiff";

Tests whether a value is an object.

FORMAT

Input

value: unknown;
  • value: the value whose type will be checked.

USAGE

Input

isObject(["hello", "world"]);

Output

false;

ℹ️ More examples are available in the source code tests.


CREDITS

DoneDeal0

SUPPORT

If you or your company uses Superdiff, please show your support by becoming a sponsor! Your name and company logo will be displayed on the README.md. Premium support is also available. https://github.com/sponsors/DoneDeal0


sponsor

CONTRIBUTING

Issues and pull requests are welcome!