2
\$\begingroup\$

(Previous and initial iteration lives here.)

(The next iteration is here.)

Now I was able to spare some lines by putting diff related state in an object. Also, I relied on Files.readAllLines, as advised.

com.github.coderodde.diff.Diff.java:

package com.github.coderodde.diff;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Objects;

public class Diff {

    private static final String NL = System.lineSeparator();
    private static final String HELP_MESSAGE = 
            "java -jar Diff.jar OLD_FILE NEW_FILE";

    private final List<String> oldSourceLines;
    private final List<String> newSourceLines;
    private final int[][] matrix;

    public Diff(List<String> oldSourceLines, List<String> newSourceLines) {
        this.oldSourceLines = Objects.requireNonNull(oldSourceLines);
        this.newSourceLines = Objects.requireNonNull(newSourceLines);
        this.matrix = new int[oldSourceLines.size() + 1]
                             [newSourceLines.size() + 1];
    }

    public String compute() {
        computeLongestCommonSubsequenceMatrix();
        return buildDiff();
    }

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println(HELP_MESSAGE);
            System.exit(0);
        }

        try {
            List<String> oldSourceLines = readFile(new File(args[0]));
            List<String> newSourceLines = readFile(new File(args[1]));

            Diff diff = new Diff(oldSourceLines, newSourceLines);
            System.out.println(diff.compute());

        } catch (IOException ex) {
            System.err.println("I/O failed: " + ex.getMessage());
            System.exit(1);
        }
    }

    // Reads all the file lines into a list and returns the list in question.
    private static List<String> readFile(File file) throws IOException {
        return Files.readAllLines(file.toPath());
    }

    // This algorithm solves the Longest Common Subsequence problem. The 
    // alphabet is not assumed to be a cuatomary alphabet, but rather all 
    // possible strings over a simple alphabet.
    private void computeLongestCommonSubsequenceMatrix() {
        for (int i = 1; i <= oldSourceLines.size(); ++i) {
            for (int j = 1; j <= newSourceLines.size(); ++j) {
                if (oldSourceLines.get(i - 1)
                        .equals(newSourceLines.get(j - 1))) {
                    matrix[i][j] = matrix[i - 1][j - 1] + 1;
                } else {
                    matrix[i][j] = Math.max(matrix[i - 1][j], 
                                            matrix[i][j - 1]);
                }
            }
        }
    }

    // The entry ponit for the algorithm that reconstructs the LCS from the 
    // matrix.
    private String buildDiff() {

        StringBuilder stringBuilder = new StringBuilder();

        buildDiff(stringBuilder,
                  oldSourceLines.size() - 1, 
                  newSourceLines.size() - 1);

        return stringBuilder.toString();
    }

    // Reconstructs the diff ocntent from the LCS matrix.
    private void buildDiff(StringBuilder stringBuilder,
                           int i, 
                           int j) {
        if (i >= 0 
                && j >= 0 
                && oldSourceLines.get(i).equals(newSourceLines.get(j))) {
            buildDiff(stringBuilder,
                      i - 1, 
                      j - 1);

            stringBuilder.append("  ").append(oldSourceLines.get(i)).append(NL);
        } else if (j > 0 && (i == 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
            buildDiff(stringBuilder,
                      i, 
                      j - 1);

            stringBuilder.append("+ ").append(newSourceLines.get(j)).append(NL);
        } else if (i > 0 && (j == 0 || matrix[i][j - 1] < matrix[i - 1][j])) {
            buildDiff(stringBuilder,
                      i - 1, 
                      j);

            stringBuilder.append("- ").append(oldSourceLines.get(i)).append(NL);
        }
    }
}

Critique request

As always, please, tell me anything that comes to mind. Am I improving my toy diff?

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Could you please add which version of Java you're using? \$\endgroup\$ Jul 4, 2022 at 17:34

1 Answer 1

4
\$\begingroup\$

This is a clear improvement from the first version, but there is still room for improvement.

Return a specific Object

However the result would benefit from being another object, or at least a list of the diff lines. Why? Because you input two lists of String and get a single String in return that people will have to parse to properly understand. This will allow you to decouple the formatting of the answer from the result itself.

I personally would return a List<Difference> where Difference contains two fields: the operation to go from oldLines to newLines (EQUAL, INSERT, DELETE) and the text. Then I would create a separate method that does only the formatting.

Use 0-index when possible

computeLongestCommonSubsequenceMatrix is one code that when you read it, you barely understand anything because everything is i-1 or j-1. The cognition of using i-1 is way higher than i+1. Okay you have some indices that are i or j, but it's more idiomatic to use for (i = 0; i < ...; i++) than for (i = 1; i <= ...; i++) because Java is always 0-based.

Also, your matrix is too big: its first row and its first column are always nothing but zeroes. So it's probably much better to revise the algorithm to remove those, and use 0-based index, I'm quite sure that the result will be more readable, as no one will then wonder why the size is +1 and why the loop starts at 1.

Use Javadoc comments to explain what the method does.

Javadoc has the Javadoc comments, /** ... */, use it, especially when your comment is explaining what the method does; and even when the method is private.

Plus, re-read your comments as they're full of typos.

Sanitize the inputs

Objects.requireNonNull() is nice, but List.copyOf() (Java 9+) also checks the content and makes sure that no element of the list is null. If any user provides null as a list element, it's only when compute is invoked that the NullPointerException is thrown. With List.copyOf(), it happens as soon as the list is provided and indicates that the class is null-hostile (which it implicitly is).

Store data in local variables

The method buildDiff is hard to understand for several reasons. One of which is because you keep repeating "oldSourceLines.get(i)" and "newSourceLines.get(j)". Those .get(x) get in the way of an easy reading, so create new variables in order to remove that cognition from the reader. Also, if the algorithm mixed sometimes i and j, there might be a reason to keep as you wrote, but in this case, it's always old->i and new->j, more reason to externalize.

It also happens that Files.readAllLines returns a RandomAccess List in the OpenJDK, but the specification is totally vague about it. If it returned a LinkedList or something similar, the implementation would be drastically slower.

Document recursion

The other reason why buildDiff is hard to understand is because it is recursive. Recursion is hard, and changing anything can break it very easily. So it's always better to make sure that recursion is properly documented.

Use Path.of()

The code currently mixes java.io and java.nio code. Try to be consistent and use only one. From your previous iteration, you chose to switch to Files.readAllLines(), meaning that you want to go towards java.nio.

Using Path.of(args[0]) (in Java 11) or Paths.get(args[1]) (Java 7+) is the way to go, now because the API is much smoother.

Also, readFile is useless as reading the lines is quite straightforward with the method you used:

List<String> oldSourceLines = Files.readAllLines(Path.of(args[0]));
List<String> newSourceLines = Files.readAllLines(Path.of(args[1]));

Rename variables

The word source in the variable name adds nothing, and even confuses the reader as they can ask "which source?". Since you lose nothing by naming the variables oldLines and newLines, it's shorter but still entirely meaningful and takes less place both in the code and the mind of the reader.

However i and j as parameters of the buildDiff recursive method are unreadable. i and j should be used in a for-loop and nowhere else. Use meaningful name here, such as oldLineIndex and newLineIndex, for instance. Ok your code will be longer, but it's more meaningful and the reader will know that you decrement an index when you -1 it: currently they don't.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.