Comparing version numbers
Some time ago, I read that comparing version numbers can be done using the following code snippet:
NSString *vesrion_1 = @"1.2.1";
NSString *version_2 = @"1.2.0";
if ([version_1 compare:version_2 options:NSNumericSearch] == NSOrderedAscending) {
...
}
This seems to work fine, but one user is having a problem, which is due to a seemingly incorrect version number comparison.
My question is, is it safe under all circumstances to use this technique to com开发者_Python百科pare version numbers? Is it possible that the above comparison results in different outcomes on different machines?
I've developed a VersionComparator on GitHub that is very light and simple to use - not as feature packed as some other solutions but easy to pick up and use.
BOOL greater = [VersionComparator isVersion:@"2.0.0" greaterThanVersion:@"1.1.0"];
It simply compares the numbers from the major to the build - if 2 is higher than 1 then there's no need to compare further.
It's aim is to provide the front-end code as simple as possible (as in the example above), and also not provide a class that has reams and reams of supporting code. More often than not, this is all that's needed.
I don't know of anything built in that will do it, but I have heard that the Sparkle framework has a version comparator.
Browsing quickly through the source reveals that the SUStandardVersionComparator object seems to be in charge of it. It conforms to the <SUVersionComparison>
protocol,which means you could probably just use it like this:
NSString *versionA = @"1.2.1";
NSString *versionB = @"1.2.0";
id <SUVersionComparison> comparator = [SUStandardVersionComparator defaultComparator];
NSInteger result = [comparator compareVersion:versionA toVersion:versionB];
if (result == NSOrderedSame) {
//versionA == versionB
} else if (result == NSOrderedAscending) {
//versionA < versionB
} else {
//versionA > versionB
}
(note: code untested and typed in a browser. Caveat Implementor)
NSNumericSearch
should do exactly what you want. It handles all the cases you would expect from a version number. For a previous project I wrote a bunch of test cases for this:
+ (NSComparisonResult)compareBundleVersion:(NSString *)a withBundleVersion:(NSString *)b
{
return [a compare:b options:NSNumericSearch];
}
STAssertEquals(NSOrderedSame, [self compareBundleVersion:@"1" withBundleVersion:@"1"], nil);
STAssertEquals(NSOrderedSame, [self compareBundleVersion:@"1.0" withBundleVersion:@"1.0"], nil);
STAssertEquals(NSOrderedSame, [self compareBundleVersion:@"1.1.12" withBundleVersion:@"1.1.12"], nil);
STAssertEquals(NSOrderedAscending, [self compareBundleVersion:@"1" withBundleVersion:@"2"], nil);
STAssertEquals(NSOrderedAscending, [self compareBundleVersion:@"1" withBundleVersion:@"1.1"], nil);
STAssertEquals(NSOrderedAscending, [self compareBundleVersion:@"1.0" withBundleVersion:@"1.1"], nil);
STAssertEquals(NSOrderedAscending, [self compareBundleVersion:@"1.1.12" withBundleVersion:@"1.1.13"], nil);
STAssertEquals(NSOrderedAscending, [self compareBundleVersion:@"1.1.12" withBundleVersion:@"1.2.1"], nil);
STAssertEquals(NSOrderedDescending, [self compareBundleVersion:@"1.1" withBundleVersion:@"1"], nil);
STAssertEquals(NSOrderedDescending, [self compareBundleVersion:@"1.1" withBundleVersion:@"1.0"], nil);
STAssertEquals(NSOrderedDescending, [self compareBundleVersion:@"1.1.13" withBundleVersion:@"1.1.12"], nil);
STAssertEquals(NSOrderedDescending, [self compareBundleVersion:@"1.2.1" withBundleVersion:@"1.1.12"], nil);
I have also tested this with the previous cases mentioned by another poster:
STAssertEquals(NSOrderedDescending, [self compareBundleVersion:@"1.30" withBundleVersion:@"1.3"], nil);
STAssertEquals(NSOrderedDescending, [self compareBundleVersion:@"1.19" withBundleVersion:@"1.3"], nil);
Everything passes as you would expect.
I'd say no, it's not safe. Version numbers are not really numbers but hierarchies of numbers. Consider for instance three version numbers:
1.19
1.3
1.30
A numeric comparison would put 1.19 as being smaller than 1.3 and 1.30. It would also say 1.3 and 1.30 are equal. If the above are version numbers, that is almost certainly not what you want.
There's also the issue of localisation*. In French, the above would not even parse as numbers.
It's far better to treat version numbers as what they are, a hierarchy of separate integers. You can easily chop them up with -componentsSeparatedByString:
*Somewhat ironically, my browser is flagging the British English spelling of localisation as being incorrect.
I like Mike's (less code) answer but this should also work even if there is a question about the compare option NSNumericSearch works.
- (NSComparisonResult)compareVersion:(NSString *)versionA to:(NSString *)versionB
{
NSArray *versionAComp = [versionA componentsSeparatedByString:@"."];
NSArray *versionBComp = [versionB componentsSeparatedByString:@"."];
__block NSComparisonResult result = NSOrderedSame;
[versionAComp enumerateObjectsUsingBlock:
^(NSString *obj, NSUInteger idx, BOOL *stop)
{
// handle abbreviated versions.
if (idx > versionBComp.count -1)
{
*stop = YES;
return;
}
NSInteger verAInt = [versionAComp[idx] integerValue];
NSInteger verBInt = [versionBComp[idx] integerValue];
if (verAInt != verBInt)
{
if (verAInt < verBInt)
result = NSOrderedAscending;
else
result = NSOrderedDescending;
*stop = YES;
return;
}
}];
return result;
}
In Java you could use the following code snippet to compare versions. If one version number is shorter then the other we normalize it with zeros.
import java.util.Arrays;
import java.util.Comparator;
public class FreePlay {
static String[] versions = {"1.5.3.2", "2.3.4", "1.0.3.4", "10.10.1.1", "1.0.2.5", "2.3.4"};
/**
* @param args
*/
public static void main(String[] args) {
System.out.println("Unsorted versions");
for (String s: versions) {
System.out.print("'" + s + "', ");
}
System.out.println("\nSorted versions");
Arrays.sort(versions, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
String[] firstVersions = o1.split("\\.");
String[] secondVersions = o2.split("\\.");
int length = Math.max(firstVersions.length, secondVersions.length);
for(int i = 0; i < length; i++) {
// normalize the length. If one part is short, we normalize it with zero
int firstVersion = i < firstVersions.length ? Integer.parseInt(firstVersions[i]) : 0;
int secondVersion = i < secondVersions.length ? Integer.parseInt(secondVersions[i]) : 0;
if(firstVersion < secondVersion)
return -1;
if(firstVersion > secondVersion)
return 1;
}
return 0;
}});
for (String s: versions) {
System.out.print("'" + s + "', ");
}
}
}
Using NSNumericSearch works in most cases but it fails when version formats are different;
e.g. when comparing
[self compareBundleVersion:@"1.1.12" withBundleVersion:@"1.2"];
it will return NSOrderedDescending while it should return NSOrderedAscending.
Please have a look at my NSString category compareToVersion which handles this in a nice way;
[@"1.2.2.4" compareToVersion:@"1.2.2.5"];
There are also a few helpers;
[@"1.2.2.4" isOlderThanVersion:@"1.2.2.5"];
[@"1.2.2.4" isNewerThanVersion:@"1.2.2.5"];
[@"1.2.2.4" isEqualToVersion:@"1.2.2.5"];
[@"1.2.2.4" isEqualOrOlderThanVersion:@"1.2.2.5"];
[@"1.2.2.4" isEqualOrNewerThanVersion:@"1.2.2.5"];
Check it out at; https://github.com/stijnster/NSString-compareToVersion
From Apple's documentation:
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) {
// Load resources for iOS 6.1 or earlier
}
else {
// Load resources for iOS 7 or later
}
精彩评论