Skip to content

fix: bug fix for highly correlated objects - #133

Open
jedluhmann wants to merge 19 commits into
andreyvit:masterfrom
jedluhmann:jl-bug-fix-for-highly-correlated-objects
Open

fix: bug fix for highly correlated objects#133
jedluhmann wants to merge 19 commits into
andreyvit:masterfrom
jedluhmann:jl-bug-fix-for-highly-correlated-objects

Conversation

@jedluhmann

@jedluhmann jedluhmann commented Sep 3, 2026

Copy link
Copy Markdown

This PR fixes a bug in the fuzzy logic that causes the array diffing logic to pick an incorrect winner for objects that have a high degree of similarity.

Test case that demonstrates this bug:
it 'should correctly pick best match based on similarity during scalarize, i.e. class obj2[2] should not be selected as best match for obj1[0], instead order should remain unchanged with added class property for obj1[0]'
See: https://github.com/jedluhmann/json-diff/blob/b6500e9d5669d9f31ebe51d26670dcbab2129bf3/test/json-diff.spec.js#L187

The objects in this test case consist of an array of 3 objects. The only difference between object A and object B is that the B0 contains a new property, class: "prose". A human looking at the two objects can easily detect that a single property was added to the object at index 0. However, the algorithm was selecting A2 as the best match for B0, which resulted in completely replacing A0 with B0.

Previously, this is how the score was determined:

  • deleted/added properties: -30
  • matching property: +20
  • matching property with equal value: +20
  • matching property with unequal value: 0 (Note: no penalty for unequal values)

The following table shows the fuzzy calculations:

 FUZZY SCORES for new obj B compared to candidate A

      A0   A1    A2    
 ----------------------
 B0   90   0     100   <-- Incorrect Winner
 B1   0    200   -     <-- Correct
 B2   50   0     400   <-- Correct

Incorrect Output:

[
  -  {
  -    type: "collection"
  -    format: "paragraph"
  -    data: [
  -      {
  -        type: "verse"
  -        vn: 2
  -      }
  -    ]
  -  }
  +  {
  +    type: "collection"
  +    format: "paragraph"
  +    class: "prose"
  +    data: [
  +      {
  +        type: "verse"
  +        vn: 2
  +      }
  +    ]
  +  }
  ...
  ...
]

So why was this the case?

Comparing B0 to A0:

 - 1 added property: -30
 - 3 matching properties: +60
 - 3 matching values: +60

 Total: 90

Comparing B0 to A3:

 - 4 matchning properties: +80
 - 1 matching value: +20

 Total: 100

The results are now determined by the following:

  • deleted/added properties: -50
  • matching property: +50
  • matching property with equal value: +50
  • mathching property with unequal value: -Math.max(0, 50 - change.score), for most cases: -40 or -50

The following table shows the new fuzzy calculations:

      A0    A1    A2    
 -----------------------
 B0   250   0     110   
 B1   0     200   -     
 B2   60    0     400   

And the now Correct Output:

[
  {
    +    class: "prose"
  }
  ...
  ...
]

Also, the goal in making this change was to make the algorithm conceptually easier to reason about. Subtracting 30 here and adding 20 here and there and then assigning a value of 100 to a matching scalar value only to later run it through score += Math.min(20, Math.max(-10, change.score / 5)); is difficult to reason about from a conceptual standpoint.

My approach was instead to think of each property of having a potential total of 100 points. This is conceptually much easier to grasp and also seems to work well. The only reduction in value that occurs due to recursion is Math.max(0, 50 - change.score) and even this is much easier to reason about. This also allows the algorithm to be progressively enhanced, if needed, by considering how similar nested objects are to one another. However, the fuzzy matching that determines the object that is the best match seems to be a much more significant contribution than the recursive similarity of nested objects. And, if possible, leaving recursive similarity out of the equation makes the entire algorithm much, much easier to reason about.

Let me know if you have any questions or need additional clarification.

Upgrades include bun, eslint, prettier, mocha, chai, and c8.
…tringify for array equality)

Some comments are left in this commit to help clarify what is being optimized. These comments will be removed in the next commit.

Changes:
- Cache changes calculated in findMatchingObject() for later use in arrayDiff where op is 'equal'
- Use JSON.stringify() to bypass diff logic when arrays are equal
- findMatchingObject: Switched the order of arguments when calling this.diff() for consistency (candidate is the original value) and to allow caching (reuse change values calculated here in later fuzzy matching)
…condition immediately above for truthy values
…for failed tests

Note: signature for deepEqual is assert.deepEqual(actual, expected, message)
Objective: Simplify the logic by giving each property a maximum possible score of 100

- Matching property: 50 points
- Matching value: 50 points
- Added/Deleted  property: -50 points
@jedluhmann

jedluhmann commented Sep 3, 2026

Copy link
Copy Markdown
Author

@andreyvit, please also note the new debug feature noted in #134. This is an incredibly useful addition.

@jedluhmann
jedluhmann force-pushed the jl-bug-fix-for-highly-correlated-objects branch 2 times, most recently from ae0619a to e0857c9 Compare September 3, 2026 22:34
This updates the README with a simple diagram that provides a high level overview of how json-diff works.
@jedluhmann
jedluhmann force-pushed the jl-bug-fix-for-highly-correlated-objects branch from e0857c9 to 097dae0 Compare September 4, 2026 05:47
@jedluhmann jedluhmann changed the title bug fix for highly correlated objects fix: bug fix for highly correlated objects Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant