Saturday, October 20, 2007

Smart Version Class with Version Increment Validation and Suggestion

For a build management system I am working on, I want a smarter way of doing version numbering for versioned builds. Right now a user can enter any version number, but I would like the system to suggest a next version number. E.g. if the last versioned build had a version number 1.2.3, I would like the system to suggest 1.2.4 as next version number when a new versioned build is started. I would also like to give a warning when a user tries to go from 1.2.3 to 1.2.5, skipping a number.

For this I have created a "smart" version class that works with numerical version numbers only and that can have any amount of version digits. Such version numbers include 1.2.3, 2.1.0, 2.33.24.18.65, etc. Valid transitions of one version number to another are from 1.2.3 to 1.2.4, 1.2.3 to 1.3.0, 1.2.3 to 1.2.3.0, 1 to 2, 1.2.3 to 2, 1.2.3 to 2.0, 1.2.3 to 2.0.0, 1 to 2.0, 2.0 to 3. The basics of this smart version class is an integer array, although that is quite the overkill, and a byte would probably have been enough (640KB anyone?). The constructor of this class takes an integer array, and per version class I want the array to become immutable (if you need a new version, instantiate a new version object).

public class Version {

  private final int[] versionDigits;
  
  public Version(int[] versionDigits) {
    if (versionDigits == null) {
      throw new IllegalArgumentException("versionDigits is null");
    }
    if (versionDigits.length == 0) {
      throw new IllegalArgumentException("versionDigits length is 0");
    }

    this.versionDigits = versionDigits;
  }

To complement the foundation, a helper method is added that is able to parse a version string such as "1.2.3" into an array of digits. This just makes our life easier when we have version strings and want to convert them into a version object. This is done by the parse method.

  public static Version parse(String versionString) {
    if (versionString == null) {
      throw new IllegalArgumentException("versionString is null");
    }
    
    StringTokenizer st = new StringTokenizer(versionString, ".");
    int[] versionDigits = new int[st.countTokens()];
    int i = 0;
    while (st.hasMoreTokens()) {
      versionDigits[i++= Integer.valueOf(st.nextToken()).intValue();
    }
    return new Version(versionDigits);
  }

Now we will get to the interesting stuff. The following method checks whether a transition was a valid one and returns true if so, otherwise false.

  public boolean isValidNextVersion(Version newVersion) {
    int i = 0;
    for (; i < versionDigits.length; i++) {
      if ((newVersion.getVersionDigits()[i- versionDigits[i]) == 1) {
        i++;
        break;
      else if (newVersion.getVersionDigits()[i!= versionDigits[i]) {
        return false;
      else if ((i + 1== newVersion.getVersionDigits().length) {
        return false;
      }
    }
    for (; i < newVersion.getVersionDigits().length; i++) {
      if (newVersion.getVersionDigits()[i!= 0) {
        return false;
      }
    }
    return true;
  }

The above method deserves some explanation. It goes through all the digits and first checks whether the difference is positive one (new digit - old digit = 1). If that's the case, then we have found the one digit that changed and that it had increased with one. We will then need to increase the position in the array with one, break out of the initial loop and start checking whether the remainder of digits are zero in the new version number. E.g. if the second digit in 1.2.3 was increased by one, the resulting version number must be 1.3.0.

If there was no difference of one, then the new digit and the old digit must be the same (e.g. in the case of the second digit in 1.2.3 to 1.2.4). If that is not the case (new digit != old digit), we know one of the digits did an invalid transition, e.g. from 1.2.3 to 1.4.0, and we can safely return false.

The last condition in the first loop checks whether the next run in the loop would cause the array index to be out of bounds for the new version number. As the first check did not break yet, we are pretty confident that the version number remained the same, e.g. from 1.2 to 1.2, or went backward losing digits, e.g. from 1.2.3 to 1.2. The condition that the old version number has less digits than the new version number is already covered by the fact that the first for loop loops through the digits of the old version number.

The second loop in the method just checks whether all remaining digits in the new version number are zeros. If that is not the case, we would have gone from e.g. 1.2.3 to 1.2.3.1 or 1.2.3 to 1.2.4.1. Return false if that's the case for any of these digits, else we can safely say the version number transition was valid and return true. As a bonus we can implement a method that suggests a new version. Because we do not want the old version number's digits to be touched, we first create a getVersionDigits method that returns a copy of the internal array:

  public int[] getVersionDigits() {
    int[] newVersionDigits = new int[versionDigits.length];
    System.arraycopy(versionDigits, 0, newVersionDigits, 0,
        versionDigits.length
);
    return newVersionDigits;
  }

The method that suggests the new version will actually return a new version object with the suggested version digits. This is done by calling the getVersionDigits method, increasing the last digit of that array by one and returning a new version number object with the given array, e.g. in the case of 1.2.3 the suggested new version is 1.2.4. Variations on this theme could include a mechanism that suggests valid transitions for any of the digits, e.g. in the case of 1.2.3 the new suggested version number would be 1.3.0.

  public Version getNextSuggestedVersion() {
    int[] newVersionDigits = getVersionDigits();
    newVersionDigits[newVersionDigits.length - 1]++;
    return new Version(newVersionDigits);
  }
}

This class is in no way set in stone. Variations on this class could be returning the position of the invalid digit transition (e.g. digit 2 did an invalid transition) or something that gives back reason codes as to why a transition was invalid (e.g. numbers are the same, new version is smaller than old version, etc.), or the ability to specify whether version numbers are zero based or one based (e.g. making 1.2.3 to 1.2.4.1 a valid transition and suggesting version numbers likewise). Maybe it is desirable to have something like a suffix as well, and requiring the suffix to change if the version number remains the same (e.g. 1.2.3 RC1, 1.2.3 final, etc.).

This can all be put into place in the code shown above. At least this class is a starting point that takes care of the nitty gritty details of the algorithm that does the validation on the version digit transitions. Although something like validating version transitions sounds simple, suffice to say my initial attempt consisted of 6 for loops and 24 if branches and that I am very happy to have been able to bring this down to what I have now.

No comments: