On this page
Description
Is "2.10.1" newer than "2.9.5"? Should "file10" sort after "file9"? Strings that contain numbers have to be compared number by number. As plain text, "2.10.1" comes before "2.9.5", because the character '1' comes before '9'. This skill covers comparing version strings segment by segment, natural sort for names that mix text and numbers, and the rules a comparator must follow to be safe to sort with.
We'll go from simple to more technical:
- The pattern: split on dots, compare segments as numbers, and treat missing segments as zero.
- A second flavor: natural sort, with digit runs compared by value.
- Where this shows up in production (package versions, file listings, release ordering).
- What breaks it: text comparison of numbers, regex dots, shorter versions, huge numbers.
The approach
Java
int compareVersion(String version1, String version2) {
String[] a = version1.split("\\."), b = version2.split("\\."); // split takes a regex: escape the dot
for (int i = 0; i < Math.max(a.length, b.length); i++) {
int x = i < a.length ? Integer.parseInt(a[i]) : 0; // a missing segment counts as 0
int y = i < b.length ? Integer.parseInt(b[i]) : 0;
if (x != y) return x < y ? -1 : 1;
}
return 0;
}
Python
def compare_version(version1: str, version2: str) -> int:
a, b = version1.split("."), version2.split(".") # str.split takes a literal: no escaping
for i in range(max(len(a), len(b))):
x = int(a[i]) if i < len(a) else 0 # a missing segment counts as 0
y = int(b[i]) if i < len(b) else 0
if x != y:
return -1 if x < y else 1
return 0
Three details make it correct. Integer.parseInt ignores leading zeros, so "01" equals "1". Looping to the longer length with an implicit 0 makes "1.0" equal "1.0.0" while "1.1" stays below "1.1.1". And the split pattern is a regex: split(".") returns an empty array (every character matches), so the loop never runs and every comparison says "equal". Python's str.split(".") takes a literal dot and just works.
A second flavor: natural sort
File names mix text and numbers anywhere: "img12.png", "img2.png". Natural sort splits each string into runs of digits and non-digits, compares digit runs by numeric value and everything else as text:
Java
int naturalCompare(String a, String b) {
int i = 0, j = 0;
while (i < a.length() && j < b.length()) {
int ei = runEnd(a, i), ej = runEnd(b, j); // end of the current digit / non-digit run
String x = a.substring(i, ei), y = b.substring(j, ej);
int c;
if (isDigit(x.charAt(0)) && isDigit(y.charAt(0))) {
String sx = stripZeros(x), sy = stripZeros(y); // "007" -> "7"
c = sx.length() != sy.length() ? Integer.compare(sx.length(), sy.length()) : sx.compareTo(sy);
} else {
c = x.compareTo(y);
}
if (c != 0) return c < 0 ? -1 : 1;
i = ei;
j = ej;
}
return Boolean.compare(i < a.length(), j < b.length()); // the one with runs left is larger
}
Python
import re
def natural_compare(a: str, b: str) -> int:
runs_a = re.findall(r"[0-9]+|[^0-9]+", a) # maximal digit / non-digit runs
runs_b = re.findall(r"[0-9]+|[^0-9]+", b)
for x, y in zip(runs_a, runs_b):
if x.isdigit() and y.isdigit():
x, y = int(x), int(y) # "007" == 7: compare by value
if x != y:
return -1 if x < y else 1
return (len(runs_a) > len(runs_b)) - (len(runs_a) < len(runs_b)) # the one with runs left is larger
Comparing digit runs by length (after stripping zeros) and then character by character works for numbers of any size. Long.parseLong throws NumberFormatException on a 21-digit run. Python's int() parses it fine (up to 4,300 digits by default), so there digit runs can simply be compared as ints.
Where you'll see it
Package managers compare dependency versions this way (Maven's ComparableVersion, npm's semver); file managers sort "file2" before "file10" (Windows Explorer's StrCmpLogicalW, macOS Finder). Semantic Versioning adds rules on top: a pre-release ranks below its release (1.0.0-alpha < 1.0.0), and build metadata after + is ignored for precedence.
Which tool for which job?
| You need | Use |
|---|---|
| Compare dotted versions | split on a literal dot, compare segments numerically, missing ones as 0 |
| Names mixing text and numbers | natural sort: digit runs by value, the rest as text |
| Numbers too large for a long | compare by stripped length, then digits |
| Semantic Versioning pre-releases | SemVer's identifier rules (numeric below alphanumeric) |
| A comparator for a TreeSet or TreeMap | break ties so distinct items never compare equal |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Comparing versions as text | "1.10" sorts before "1.2" | compare segments as numbers |
split(".") in Java | a regex wildcard: no segments | split("\\.") |
| Stopping at the shorter version | "1.1" equals "1.1.1" | missing segments count as 0 |
parseLong on long digit runs | NumberFormatException | compare by stripped length, then text |
| A comparator where "a01" equals "a1" | a TreeSet silently keeps only one | add a tie-break |
| A "close enough" comparator | "Comparison method violates its general contract!" | a strict total order |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Why does "2.10.1".compareTo("2.9.5") say 2.10.1 is smaller?
Show answer
String comparison goes character by character: "2." matches, then '1' < '9', so it stops there - it never sees that 10 > 9. Versions must be compared segment by segment as numbers.
2. Why do "1.0" and "1.0.0" compare equal while "1.1" is below "1.1.1"?
Show answer
The loop runs to the longer version's segment count and treats a missing segment as 0. "1.0" vs "1.0.0" compares 1=1, 0=0, 0=0; "1.1" vs "1.1.1" reaches 0 < 1 in the third segment.
3. How do you compare two 30-digit runs without overflow?
Show answer
Strip leading zeros, then compare lengths - a longer number is larger - and, if the lengths are equal, compare the digit strings character by character, which matches numeric order for equal-length numbers.
Key takeaways
- Compare numbers inside strings as numbers, segment by segment.
split("\\."), notsplit(".").- Missing trailing segments count as 0; leading zeros don't count.
- Natural sort: digit runs numerically (length first, then digits), other runs as text.
- Big digit runs overflow
parseLong; compare by length instead.