How to compare two app version strings in Swift

Swift String

Comparing an app version is an easy task for a human. You can tell right away which version is newer, e.g., 2.1.0 is higher than 1.0.0 and 2.0.0, but it might not easy when you try to do this in code. You might try to do it manually with the help of string splitting. What you might not aware of is, if you version strings have a consistent format, you can compare it with a standard String method built right in the Foundation framework.

Version Strings #

In this post, I will focus on iOS version number.

From CFBundleShortVersionString

The release version number is a string composed of three period-separated integers. The first integer represents major revision to the app, such as a revision that implements new features or major changes. The second integer denotes a revision that implements less prominent features. The third integer represents a maintenance release revision.

The following are some valid examples:

1.0
1.0.3
2.0.0

Comparing #

To compare app version strings, you can use compare(_:options:range:) with an option of .numeric.

let version1 = "1.0.0"
let version2 = "2.0.0"
version1.compare(version2, options: .numeric)
// .orderedAscending

For brevity, I will create a String extension to wrap compare(_:options:range:) and call it versionCompare(_:).

extension String {
func versionCompare(_ otherVersion: String) -> ComparisonResult {
return self.compare(otherVersion, options: .numeric)
}
}

Some examples of usage:

"1.0.0".versionCompare("1.0.0") // .orderedSame
"0.0.2".versionCompare("0.0.1") // .orderedDescending
"0.1".versionCompare("0.0.1") // .orderedDescending
"0.1.0".versionCompare("0.0.1") // .orderedDescending
"0.2".versionCompare("0.1") // .orderedDescending
"1.0.0".versionCompare("1.1") // .orderedAscending
"0.1.0".versionCompare("1.0.0") // .orderedAscending
"0.0.1".versionCompare("1.0.0") // .orderedAscending
"0.0.1".versionCompare("1") // .orderedAscending
"1.0".versionCompare("2") // .orderedAscending

Caveat #

There is one problem with this compare(_:options:range:) method. It will give a wrong comparison result if a version is the same but in a different format.

"1.0.0".versionCompare("1")     // .orderedDescending
"1.0.0".versionCompare("1.0") // .orderedDescending
"1.0.0".versionCompare("1.0.") // .orderedDescending

Fix #

To prevent this, make sure your version strings have a consistent format (same number of period and zeros). You can do extra work to make sure your version strings have the same period and zeros. The following is my adjustment to versionCompare(_:) to do just that.

func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "."

var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)

let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>

if zeroDiff == 0 { // <3>
// Same format, compare normally
return self.compare(otherVersion, options: .numeric)
} else {
let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4>
if zeroDiff > 0 {
otherVersionComponents.append(contentsOf: zeros) // <5>
} else {
versionComponents.append(contentsOf: zeros)
}
return versionComponents.joined(separator: versionDelimiter)
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
}
}

<1> We split the version by period (.).
<2> Then, we find the difference of digit that we will zero pad.
<3> If there are no differences, we don't need to do anything and use simple .compare.
<4> We populate an array of missing zero.
<5> We add zero pad array to a version with a fewer period and zero.
<6> We user array components to build back our versions from components and compare them. This time it will have the same period and number of digit.

This method will make sure we two version strings got an identical period and the same number of digit before making the comparison.


Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy this article, you can subscribe to the weekly newsletter.

Every Friday, you’ll get a quick recap of all articles and tips posted on this site — entirely for free.

← Home