ra1028 / DifferenceKit

💻 A fast and flexible O(n) difference algorithm framework for Swift collection.

Home Page:https://ra1028.github.io/DifferenceKit

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unnecessary Moves

kylerobson opened this issue · comments

Checklist

Example Test Scenario

    func testMovedIssue() {
        let section = 1
        
        let source1 = [1, 2, 3]
        let target1 = [3, 2, 1]

        // Test1: This test passes, but it should fail.
        XCTAssertExactDifferences(
            source: source1,
            target: target1,
            section: section,
            expected: [
                Changeset(
                    data: target1,
                    elementMoved: [
                        (source: ElementPath(element: 2, section: section), target: ElementPath(element: 0, section: section)),
                        (source: ElementPath(element: 1, section: section), target: ElementPath(element: 1, section: section)),
                    ]
                )
            ]
        )
        
        // Test2: This test fails, but it should pass.
        XCTAssertExactDifferences(
            source: source1,
            target: target1,
            section: section,
            expected: [
                Changeset(
                    data: target1,
                    elementMoved: [
                        (source: ElementPath(element: 2, section: section), target: ElementPath(element: 0, section: section)),
                    ]
                )
            ]
        )
    }

Expected Behavior

Test1 should fail, Test2 should pass.

Moves contains (2 -> 0), (1 -> 1).

Current Behavior

Test1 passes, Test2 fails.

Moves should contain (2 -> 0).

Steps to Reproduce

Add the test I wrote above and run the unit tests.

Detailed Description (Include Screenshots)

Moving the item from the same index to the same index should always be unnecessary. In general optimizing to make moves correct and minimal is preferred.

Environments

  • Library version: Master

  • Swift version: 4.2

  • iOS version: 12

  • Xcode version: 10.1

  • Devices/Simulators: DifferenceKit, "My Mac", CMD+U

  • CocoaPods/Carthage version: N/A

Hi @kylerobson

It looks like correct diff and testing result.
Applying only Move(2 -> 0) to source1 results in [3, 1, 2].
The source ElementPath in the elementMoved is index of source collection, and the target ElementPath is index of target collection.
Even if the source and target indices in the Move are the same, it can't be omitted because it's affected by diffs other than Move.

I suppose what I'm reacting to is that it's possible to simply swap 3 and 1 in [1, 2, 3] -> [3, 2, 1]. If move is the only option, then two moves is the best we can do. If swap is an option, then one swap would suffice. Swap is a better option isn't it? It would avoid potentially two O(N) memory moves.

After the Move(2 -> 0) is applied, we have [3, 1, 2]

I could have up to three arrays:
[1, 2, 3] (source for the diff)
[3, 2, 1] (target for the diff)
[3, 1, 2] (current array after performing the first move)

How is Move(1 -> 1) meant to transform [3, 1, 2] to [3, 2, 1]?

Intuitively, I would receive Move(2, 1) or Move(1, 2) instead of Move(1, 1) so I can continue operating on the array being modified.

How do you actually expect people to use this?

swap is intuitive but not practical because it's not supported by UIKit or AppKit API.
As already I commented, Move's source index is that of the source array.
Therefore, [3, 1, 2] to [3, 2, 1] is the wrong explanation.
You can be understood if you think in the following steps.

  1. Decompose Move(2, 1) to delete(3, at: 2 of source array), insert(3, at: 0 of target array)
  2. Decompose Move(1, 1) to delete(2, at: 1 of source array), insert(2, at: 1 of target array)
  3. And apply all diffs to [1, 2, 3] simultaneously.
    -> delete(3, at: 2 of source array): [1, 2]
    -> insert(3, at: 0 of target array): [3, 1, 2]
    -> delete(2, at: 1 of source array): [3, 1]
    -> insert(2, at: 1 of target array): [3, 2, 1]

This work is probably similar to the internal implementation of perfromBatchUpdates.
This theory is bit difficult to understanding, I coded it for repeatability testing here.

Closing due to inactivity.
Feel free to reopen if you still have a question.

Hi @ra1028 , i have two questions:

1

And apply all diffs to [1, 2, 3] simultaneously.
-> delete(3, at: 2 of source array): [1, 2]
-> insert(3, at: 0 of target array): [3, 1, 2]
-> delete(2, at: 1 of source array): [3, 1]
-> insert(2, at: 1 of target array): [3, 2, 1]

-> delete(3, at: 2 of source array): [1, 2]: straightforward, right now source array is [1, 2].

-> insert(3, at: 0 of target array): [3, 1, 2]: target array normal is [1, 2, 3] then insert 3 at pos 0, become [3, 1, 2, 3], delete idx 3 element, so target array is [3, 1, 2]?

-> delete(2, at: 1 of source array): [3, 1]: how can source array is [3, 2, 1]?

-> insert(2, at: 1 of target array): [3, 2, 1]: target array normal is [3, 1, 2], insert 2 at pos 1, become [3, 2, 1, 2], delete idx 3 element, so target array is [3, 2, 1]? If not i can't think of else possible case.

I don't understand what it means to operate on the target array? Isn't it possible to make source become target after performing a set of operations on source?

2

for example, source is [a, b, c, d], target is [b, a, d, c]. If use React element diff algorithm, we got steps:

1. b don't move;
2. a move after b;
3. d don't move;
4. c move after d;

source performs steps and then becomes target.

then i use same source and target in this algorithm, got changeSet's elementMoved like below:

 (source: [element: 1, section: 0], target: [element: 0, section: 0]) -> moveRow(IndexPath(1, 0), IndexPath(0, 0))

 (source: [element: 3, section: 0], target: [element: 2, section: 0]) -> moveRow(IndexPath(3, 0), IndexPath(2, 0))

as same as steps like below?

move b before a;
move d before c;

If not, How can tableView use elementMove update original elements to target elements? May be there is another way that differ from react's way(source execute steps then become target) let tableView updated?


please give some hint, thanks.

@ra1028 , please see above :)

@longjianjiang

1

My explanation maybe to have confused you.

delete(3, at: 2 of source array): [1, 2]
This means that the element 3 index for the source array is 2, and the deleted result is [1, 2].
insert(3, at: 0 of target array): [3, 1, 2]
Then, insert the element 3 to [1, 2], the index 0 is pointed to the position in the final target array. Result is [3, 1, 2]
delete(2, at: 1 of source array): [3, 1]
Delete the element 2 from [3, 1, 2]. The index is pointed to the position of the source array before change. Result is [3, 1].
insert(2, at: 1 of target array): [3, 2, 1]
Finally, insert the element 2 to [3, 1]. The index is pointed to the position of the final target array after changed. Result is [3, 2, 1].

You should think that it's strange because the index should have shifted by delete or insert.
But that is correct, because these steps are operated simultaneously, not sequentially, by performBatchUpdate.

This is an important concept of the diffing algorithm and it's difficult for me to explain.
I recommend to read the algorithm code of DifferenceKit.
https://github.com/ra1028/DifferenceKit/blob/master/Sources/Algorithm.swift#L445
The code for reproducibility testing will maybe also help you understand.
https://github.com/ra1028/DifferenceKit/blob/master/Tests/TestTools.swift#L132

2

In that case, as you say, the steps are as follows:

1. a don't move;
2. b move before a;
3. c don't move;
4. d move before c;

These show different diffs, but the results are the same.
I'm not familiar with React algorithm, but I guess DifferenceKit and React are using different algorithms.
There are many types of diffing algorithms and detailed spec differences.

@ra1028 Thank you for your answer :)