Skip to content

Commit 82ce585

Browse files
committed
VersionUtils: add a method to compare versions
This compares two version strings by splitting them by dot, then comparing each split token numerically and/or lexicographically as appropriate. SemVer version strings will compare as expected, although the algorithm used is not limited to SemVer-compliant strings. This commit is dedicated to Stefan Helfrich.
1 parent 6df8c5a commit 82ce585

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-0
lines changed

src/main/java/org/scijava/util/VersionUtils.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,57 @@ public static String getBuildNumber(final Class<?> c) {
114114
return m == null ? null : m.getImplementationBuild();
115115
}
116116

117+
/**
118+
* Compares two version strings.
119+
* @param v1 The first version string.
120+
* @param v2 The second version string.
121+
* @return a negative integer, zero, or a positive integer as the
122+
* first argument is less than, equal to, or greater than the
123+
* second.
124+
*/
125+
public static int compare(final String v1, final String v2) {
126+
final String[] t1 = splitDots(v1), t2 = splitDots(v2);
127+
final int count = Math.min(t1.length, t2.length);
128+
for (int t=0; t<count; t++) {
129+
final int c = compareToken(t1[t], t2[t]);
130+
if (c != 0) return c;
131+
}
132+
if (t1.length == t2.length) return 0;
133+
// NB: Token count differs. More tokens means newer -- e.g. 1.5 < 1.5.1.
134+
return t1.length < t2.length ? -1 : 1;
135+
}
136+
137+
// -- Helper methods --
138+
139+
/** Splits the given version string by dots. */
140+
private static String[] splitDots(final String s) {
141+
// NB: -1 split limit causes split not to remove empty values.
142+
// See: https://stackoverflow.com/a/14602089/1207769
143+
return s.isEmpty() ? new String[0] : s.split("\\.", -1);
144+
}
145+
146+
/** Compares one token of a multi-token version string. */
147+
private static int compareToken(final String t1, final String t2) {
148+
final int i1 = digitIndex(t1), i2 = digitIndex(t2);
149+
if (i1 > 0 && i2 > 0) {
150+
// Versions start with digits; compare them numerically.
151+
final long d1 = Long.parseLong(t1.substring(0, i1));
152+
final long d2 = Long.parseLong(t2.substring(0, i2));
153+
if (d1 < d2) return -1;
154+
if (d1 > d2) return 1;
155+
}
156+
// Compare remaining characters lexicographically.
157+
return t1.substring(i1).compareTo(t2.substring(i2));
158+
}
159+
160+
/** Gets the subsequent index to all the given string's leading digits. */
161+
private static int digitIndex(final String s) {
162+
int index = 0;
163+
for (int i=0; i<s.length(); i++) {
164+
final char ch = s.charAt(index);
165+
if (ch < '0' || ch > '9') break;
166+
index++;
167+
}
168+
return index;
169+
}
117170
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* #%L
3+
* SciJava Common shared library for SciJava software.
4+
* %%
5+
* Copyright (C) 2009 - 2017 Board of Regents of the University of
6+
* Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck
7+
* Institute of Molecular Cell Biology and Genetics, University of
8+
* Konstanz, and KNIME GmbH.
9+
* %%
10+
* Redistribution and use in source and binary forms, with or without
11+
* modification, are permitted provided that the following conditions are met:
12+
*
13+
* 1. Redistributions of source code must retain the above copyright notice,
14+
* this list of conditions and the following disclaimer.
15+
* 2. Redistributions in binary form must reproduce the above copyright notice,
16+
* this list of conditions and the following disclaimer in the documentation
17+
* and/or other materials provided with the distribution.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
23+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
* POSSIBILITY OF SUCH DAMAGE.
30+
* #L%
31+
*/
32+
33+
package org.scijava.util;
34+
35+
import static org.junit.Assert.assertEquals;
36+
import static org.junit.Assert.assertTrue;
37+
38+
import org.junit.Test;
39+
40+
/**
41+
* Tests {@link VersionUtils}.
42+
*
43+
* @author Curtis Rueden
44+
*/
45+
public class VersionUtilsTest {
46+
47+
/** Tests {@link VersionUtils#compare(String, String)}. */
48+
@Test
49+
public void testCompare() {
50+
// SemVer PATCH version.
51+
assertTrue(VersionUtils.compare("1.5.1", "1.5.2") < 0);
52+
assertTrue(VersionUtils.compare("1.5.2", "1.5.1") > 0);
53+
54+
// SemVer MINOR version.
55+
assertTrue(VersionUtils.compare("1.5.2", "1.6.2") < 0);
56+
assertTrue(VersionUtils.compare("1.6.2", "1.5.2") > 0);
57+
58+
// SemVer MAJOR version.
59+
assertTrue(VersionUtils.compare("1.7.3", "2.7.3") < 0);
60+
assertTrue(VersionUtils.compare("2.7.3", "1.7.3") > 0);
61+
62+
// Suffix indicates version is older than final release.
63+
assertTrue(VersionUtils.compare("1.5.2", "1.5.2-beta-1") < 0);
64+
65+
// Check when number of version tokens does not match.
66+
assertTrue(VersionUtils.compare("1.5", "1.5.1") < 0);
67+
assertTrue(VersionUtils.compare("1.5.1", "1.5") > 0);
68+
69+
// Check equality.
70+
assertEquals(VersionUtils.compare("1.5", "1.5"), 0);
71+
72+
// Check ImageJ 1.x style versions.
73+
assertTrue(VersionUtils.compare("1.50a", "1.50b") < 0);
74+
75+
// Check four version tokens.
76+
assertTrue(VersionUtils.compare("1.5.1.3", "1.5.1.4") < 0);
77+
assertTrue(VersionUtils.compare("1.5.1.6", "1.5.1.5") > 0);
78+
assertEquals(VersionUtils.compare("10.4.9.8", "10.4.9.8"), 0);
79+
80+
// Check non-numeric tokens.
81+
assertTrue(VersionUtils.compare("a.b.c", "a.b.d") < 0);
82+
83+
// Check for numerical (not lexicographic) comparison.
84+
assertTrue(VersionUtils.compare("2.0", "23.0") < 0);
85+
assertTrue(VersionUtils.compare("23.0", "2.0") > 0);
86+
assertTrue(VersionUtils.compare("3.0", "23.0") < 0);
87+
assertTrue(VersionUtils.compare("23.0", "3.0") > 0);
88+
89+
// Check weird stuff.
90+
assertTrue(VersionUtils.compare("1", "a") < 0);
91+
assertTrue(VersionUtils.compare("", "1") < 0);
92+
assertTrue(VersionUtils.compare("1", "1.") < 0);
93+
assertTrue(VersionUtils.compare("", ".") < 0);
94+
assertTrue(VersionUtils.compare("", "..") < 0);
95+
assertTrue(VersionUtils.compare(".", "..") < 0);
96+
}
97+
}

0 commit comments

Comments
 (0)