2

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 compare version numbers? Is it possible that the above comparison results in different outcomes on different machines?

stacktrace
  • 23
  • 1
  • 5

8 Answers8

4

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.

Dan Hanly
  • 7,829
  • 13
  • 73
  • 134
4

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.

Mike Weller
  • 45,401
  • 15
  • 131
  • 151
  • Actually I would expect the last test to fail but it doesn't. `NSNumericSearch` must not understand decimals. – JeremyP Mar 19 '13 at 09:03
  • 1
    NSNumericSearch is specifically designed to handle strings this way. It is used by the finder to sort filenames, for example. – Mike Weller Mar 19 '13 at 12:22
3

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)

Dave DeLong
  • 242,470
  • 58
  • 448
  • 498
  • +1 for not trying to recode what seems to be a simple problem, but has many subtleties, where code that has been tested is available. – Guillaume Mar 02 '11 at 10:06
  • Thanks, Dave. After taking a look at the code (of the Sparkle framework), it is clear that it is indeed a more robust way to compare version numbers. – stacktrace Mar 02 '11 at 10:24
  • SUStandardVersionComparator is undefined for me. I looked through the framework and I don't see the respective header file. But I looked through the source and I see the header. How do I add the SUStandardVersionComparator class to the framework? – rocky Jul 11 '13 at 04:51
  • beware of using the SUStandardVersionComparator in your apps, it seems to think that "2.1.0." is not equal to "2.1" but that is newer, which i find plain wrong. – user1259710 Apr 10 '18 at 19:41
2

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.

JeremyP
  • 84,577
  • 15
  • 123
  • 161
  • Thanks, Jeremy. I have seen this approach in a few places, but as you point out, it isn't bulletproof. – stacktrace Mar 02 '11 at 10:25
  • Actually `NSNumerciSearch` does do the correct thing. I have a bunch of test cases for this which I'll put in an answer. – Mike Weller Mar 18 '13 at 16:16
1

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; 
}
possen
  • 8,596
  • 2
  • 39
  • 48
0

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 + "', ");
        }
    }

}
Rasoul
  • 167
  • 1
  • 2
  • 9
0

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

Stijnster
  • 209
  • 2
  • 7
-1

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
}
drct
  • 361
  • 4
  • 12