Efficient way to compare version strings in Java

JavaStringCompare

Java Problem Overview


> Possible Duplicate:
> How do you compare two version Strings in Java?

I've 2 strings which contains version information as shown below:

str1 = "1.2"
str2 = "1.1.2"

Now, can any one tell me the efficient way to compare these versions inside strings in Java & return 0 , if they're equal, -1, if str1 < str2 & 1 if str1>str2.

Java Solutions


Solution 1 - Java

Requires commons-lang3-3.8.1.jar for string operations.

/**
 * Compares two version strings. 
 * 
 * Use this instead of String.compareTo() for a non-lexicographical 
 * comparison that works for version strings. e.g. "1.10".compareTo("1.6").
 * 
 * @param v1 a string of alpha numerals separated by decimal points. 
 * @param v2 a string of alpha numerals separated by decimal points.
 * @return The result is 1 if v1 is greater than v2. 
 *         The result is 2 if v2 is greater than v1. 
 *         The result is -1 if the version format is unrecognized. 
 *         The result is zero if the strings are equal.
 */
    
public int VersionCompare(String v1,String v2)
{
    int v1Len=StringUtils.countMatches(v1,".");
    int v2Len=StringUtils.countMatches(v2,".");

    if(v1Len!=v2Len)
    {
        int count=Math.abs(v1Len-v2Len);
        if(v1Len>v2Len)
            for(int i=1;i<=count;i++)
                v2+=".0";
        else
            for(int i=1;i<=count;i++)
                v1+=".0";
    }

    if(v1.equals(v2))
        return 0;
    
    String[] v1Str=StringUtils.split(v1, ".");
    String[] v2Str=StringUtils.split(v2, ".");
    for(int i=0;i<v1Str.length;i++)
    {
        String str1="",str2="";
        for (char c : v1Str[i].toCharArray()) {
            if(Character.isLetter(c))
            {
                int u=c-'a'+1;
                if(u<10)
                    str1+=String.valueOf("0"+u);
                else
                    str1+=String.valueOf(u);
            }
            else
                str1+=String.valueOf(c);
        }            
        for (char c : v2Str[i].toCharArray()) {
            if(Character.isLetter(c))
            {
                int u=c-'a'+1;
                if(u<10)
                    str2+=String.valueOf("0"+u);
                else
                    str2+=String.valueOf(u);
            }
            else
                str2+=String.valueOf(c);
        }
        v1Str[i]="1"+str1;
        v2Str[i]="1"+str2;

            int num1=Integer.parseInt(v1Str[i]);
            int num2=Integer.parseInt(v2Str[i]);
            
            if(num1!=num2)
            {
                if(num1>num2)
                    return 1;
                else
                    return 2;
            }
    }
    return -1;
}    

Solution 2 - Java

As others have pointed out, String.split() is a very easy way to do the comparison you want, and Mike Deck makes the excellent point that with such (likely) short strings, it probably won't matter much, but what the hey! If you want to make the comparison without manually parsing the string, and have the option of quitting early, you could try the java.util.Scanner class.

public static int versionCompare(String str1, String str2) {
	try ( Scanner s1 = new Scanner(str1);
          Scanner s2 = new Scanner(str2);) {
        s1.useDelimiter("\\.");
        s2.useDelimiter("\\.");
    
        while (s1.hasNextInt() && s2.hasNextInt()) {
            int v1 = s1.nextInt();
            int v2 = s2.nextInt();
            if (v1 < v2) {
                return -1;
            } else if (v1 > v2) {
                return 1;
            }
        }

        if (s1.hasNextInt() && s1.nextInt() != 0)
            return 1; //str1 has an additional lower-level version number
        if (s2.hasNextInt() && s2.nextInt() != 0)
            return -1; //str2 has an additional lower-level version 

        return 0;
    } // end of try-with-resources
}

Solution 3 - Java

This is almost certainly not the most efficient way to do it, but given that version number strings will almost always be only a few characters long I don't think it's worth optimizing further:

public static int compareVersions(String v1, String v2) {
	String[] components1 = v1.split("\\.");
	String[] components2 = v2.split("\\.");
	int length = Math.min(components1.length, components2.length);
	for(int i = 0; i < length; i++) {
		int result = new Integer(components1[i]).compareTo(Integer.parseInt(components2[i]));
		if(result != 0) {
			return result;
		}
	}
	return Integer.compare(components1.length, components2.length);
}

Solution 4 - Java

I was looking to do this myself and I see three different approaches to doing this, and so far pretty much everyone is splitting the version strings. I do not see doing that as being efficient, though code size wise it reads well and looks good.

Approaches:

  1. Assume an upper limit to the number of sections (ordinals) in a version string as well as a limit to the value represented there. Often 4 dots max, and 999 maximum for any ordinal. You can see where this is going, and it's going towards transforming the version to fit into a string like: "1.0" => "001000000000" with string format or some other way to pad each ordinal. Then do a string compare.
  2. Split the strings on the ordinal separator ('.') and iterate over them and compare a parsed version. This is the approach demonstrated well by Alex Gitelman.
  3. Comparing the ordinals as you parse them out of the version strings in question. If all strings were really just pointers to arrays of characters as in C then this would be the clear approach (where you'd replace a '.' with a null terminator as it's found and move some 2 or 4 pointers around.

Thoughts on the three approaches:

  1. There was a blog post linked that showed how to go with 1. The limitations are in version string length, number of sections and maximum value of the section. I don't think it's crazy to have such a string that breaks 10,000 at one point. Additionally most implementations still end up splitting the string.
  2. Splitting the strings in advance is clear to read and think about, but we are going through each string about twice to do this. I'd like to compare how it times with the next approach.
  3. Comparing the string as you split it give you the advantage of being able to stop splitting very early in a comparison of: "2.1001.100101.9999998" to "1.0.0.0.0.0.1.0.0.0.1". If this were C and not Java the advantages could go on to limit the amount of memory allocated for new strings for each section of each version, but it is not.

I didn't see anyone giving an example of this third approach, so I'd like to add it here as an answer going for efficiency.

public class VersionHelper {
    
    /**
     * Compares one version string to another version string by dotted ordinals.
     * eg. "1.0" > "0.09" ; "0.9.5" < "0.10",
     * also "1.0" < "1.0.0" but "1.0" == "01.00"
     *
     * @param left  the left hand version string
     * @param right the right hand version string
     * @return 0 if equal, -1 if thisVersion &lt; comparedVersion and 1 otherwise.
     */
    public static int compare(@NotNull String left, @NotNull String right) {
        if (left.equals(right)) {
            return 0;
        }
        int leftStart = 0, rightStart = 0, result;
        do {
            int leftEnd = left.indexOf('.', leftStart);
            int rightEnd = right.indexOf('.', rightStart);
            Integer leftValue = Integer.parseInt(leftEnd < 0
                    ? left.substring(leftStart)
                    : left.substring(leftStart, leftEnd));
            Integer rightValue = Integer.parseInt(rightEnd < 0
                    ? right.substring(rightStart)
                    : right.substring(rightStart, rightEnd));
            result = leftValue.compareTo(rightValue);
            leftStart = leftEnd + 1;
            rightStart = rightEnd + 1;
        } while (result == 0 && leftStart > 0 && rightStart > 0);
        if (result == 0) {
            if (leftStart > rightStart) {
                return containsNonZeroValue(left, leftStart) ? 1 : 0;
            }
            if (leftStart < rightStart) {
                return containsNonZeroValue(right, rightStart) ? -1 : 0;
            }
        }
        return result;
    }
    
    private static boolean containsNonZeroValue(String str, int beginIndex) {
        for (int i = beginIndex; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c != '0' && c != '.') {
                return true;
            }
        }
        return false;
    }
}

Unit test demonstrating expected output.

public class VersionHelperTest {
    
    @Test
    public void testCompare() throws Exception {
        assertEquals(1, VersionHelper.compare("1", "0.9"));
        assertEquals(1, VersionHelper.compare("0.0.0.2", "0.0.0.1"));
        assertEquals(1, VersionHelper.compare("1.0", "0.9"));
        assertEquals(1, VersionHelper.compare("2.0.1", "2.0.0"));
        assertEquals(1, VersionHelper.compare("2.0.1", "2.0"));
        assertEquals(1, VersionHelper.compare("2.0.1", "2"));
        assertEquals(1, VersionHelper.compare("0.9.1", "0.9.0"));
        assertEquals(1, VersionHelper.compare("0.9.2", "0.9.1"));
        assertEquals(1, VersionHelper.compare("0.9.11", "0.9.2"));
        assertEquals(1, VersionHelper.compare("0.9.12", "0.9.11"));
        assertEquals(1, VersionHelper.compare("0.10", "0.9"));
        assertEquals(0, VersionHelper.compare("0.10", "0.10"));
        assertEquals(-1, VersionHelper.compare("2.10", "2.10.1"));
        assertEquals(-1, VersionHelper.compare("0.0.0.2", "0.1"));
        assertEquals(1, VersionHelper.compare("1.0", "0.9.2"));
        assertEquals(1, VersionHelper.compare("1.10", "1.6"));
        assertEquals(0, VersionHelper.compare("1.10", "1.10.0.0.0.0"));
        assertEquals(1, VersionHelper.compare("1.10.0.0.0.1", "1.10"));
        assertEquals(0, VersionHelper.compare("1.10.0.0.0.0", "1.10"));
        assertEquals(1, VersionHelper.compare("1.10.0.0.0.1", "1.10"));
    }
}

Solution 5 - Java

Split the String on "." or whatever your delimeter will be, then parse each of those tokens to the Integer value and compare.

int compareStringIntegerValue(String s1, String s2, String delimeter)  
{  
   String[] s1Tokens = s1.split(delimeter);  
   String[] s2Tokens = s2.split(delimeter);  
   
   int returnValue = 0;
   if(s1Tokens.length > s2Tokens.length)  
   {  
       for(int i = 0; i<s1Tokens.length; i++)  
       {  
          int s1Value = Integer.parseString(s1Tokens[i]);  
          int s2Value = Integer.parseString(s2Tokens[i]);  
          Integer s1Integer = new Integer(s1Value);  
          Integer s2Integer = new Integer(s2Value);  
          returnValue = s1Integer.compareTo(s2Value);
          if( 0 == isEqual)  
           {  
              continue; 
           }  
           return returnValue;  //end execution
        }
           return returnValue;  //values are equal
 } 

I will leave the other if statement as an exercise.

Solution 6 - Java

Comparing version strings can be a mess; you're getting unhelpful answers because the only way to make this work is to be very specific about what your ordering convention is. I've seen one relatively short and complete version comparison function on a blog post, with the code placed in the public domain- it isn't in Java but it should be simple to see how to adapt this.

Solution 7 - Java

Adapted from Alex Gitelman's answer.

int compareVersions( String str1, String str2 ){

    if( str1.equals(str2) ) return 0; // Short circuit when you shoot for efficiency

    String[] vals1 = str1.split("\\.");
    String[] vals2 = str2.split("\\.");

    int i=0;

    // Most efficient way to skip past equal version subparts
    while( i<vals1.length && i<val2.length && vals[i].equals(vals[i]) ) i++;

    // If we didn't reach the end,

    if( i<vals1.length && i<val2.length )
        // have to use integer comparison to avoid the "10"<"1" problem
        return Integer.valueOf(vals1[i]).compareTo( Integer.valueOf(vals2[i]) );

    if( i<vals1.length ){ // end of str2, check if str1 is all 0's
        boolean allZeros = true;
        for( int j = i; allZeros & (j < vals1.length); j++ )
            allZeros &= ( Integer.parseInt( vals1[j] ) == 0 );
        return allZeros ? 0 : -1;
    }

    if( i<vals2.length ){ // end of str1, check if str2 is all 0's
        boolean allZeros = true;
        for( int j = i; allZeros & (j < vals2.length); j++ )
            allZeros &= ( Integer.parseInt( vals2[j] ) == 0 );
        return allZeros ? 0 : 1;
    }

    return 0; // Should never happen (identical strings.)
}

So as you can see, not so trivial. Also this fails when you allow leading 0's, but I've never seen a version "1.04.5" or w/e. You would need to use integer comparison in the while loop to fix that. This gets even more complex when you mix letters with numbers in the version strings.

Solution 8 - Java

Step1 : Use StringTokenizer in java with dot as delimiter

StringTokenizer(String str, String delimiters) or

You can use String.split() and Pattern.split(), split on dot and then convert each String to Integer using Integer.parseInt(String str)

Step 2: Compare integer from left to right.

Solution 9 - Java

Split them into arrays and then compare.

// check if two strings are equal. If they are return 0;
String[] a1;

String[] a2;

int i = 0;

while (true) {
    if (i == a1.length && i < a2.length) return -1;
    else if (i < a1.length && i == a2.length) return 1;
  
    if (a1[i].equals(a2[i]) {
       i++;
       continue;
    }
     return a1[i].compareTo(a2[i];
}
return 0;

Solution 10 - Java

I would divide the problem in two, formating and comparing. If you can assume that the format is correct, then comparing only numbers version is very simple:

final int versionA = Integer.parseInt( "01.02.00".replaceAll( "\\.", "" ) );
final int versionB = Integer.parseInt( "01.12.00".replaceAll( "\\.", "" ) );

Then both versions can be compared as integers. So the "big problem" is the format, but that can have many rules. In my case i just complete a minimum of two pair of digits, so the format is "99.99.99" always, and then i do the above conversion; so in my case the program logic is in the formatting, and not in the version comparison. Now, if you are doing something very specific and maybe you can trust the origin of the version string, maybe you just can check the length of the version string and then just do the int conversion... but i think it's a best practice to make sure the format is as expected.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMikeView Question on Stackoverflow
Solution 1 - JavaAlex GitelmanView Answer on Stackoverflow
Solution 2 - JavaEricView Answer on Stackoverflow
Solution 3 - JavaMike DeckView Answer on Stackoverflow
Solution 4 - JavadlamblinView Answer on Stackoverflow
Solution 5 - JavaWoot4MooView Answer on Stackoverflow
Solution 6 - JavaProdicusView Answer on Stackoverflow
Solution 7 - JavatruthealityView Answer on Stackoverflow
Solution 8 - JavaFarmView Answer on Stackoverflow
Solution 9 - JavafastcodejavaView Answer on Stackoverflow
Solution 10 - JavaPatricio TéllezView Answer on Stackoverflow