Skip to content

Commit b66c881

Browse files
committed
improve prerelease version comparison checking
1 parent 7c53996 commit b66c881

File tree

3 files changed

+125
-11
lines changed

3 files changed

+125
-11
lines changed

src/main/java/com/github/khakers/modmailviewer/UpdateChecker.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class UpdateChecker {
2121

2222
private final Version appVersion = new Version(ModmailViewer.TAG);
2323

24-
private final boolean isPrereleaseEligible = ModmailViewer.BRANCH.equalsIgnoreCase("develop") || (appVersion.prerelease != null && !appVersion.prerelease.isBlank());
24+
private final boolean isPrereleaseEligible = ModmailViewer.BRANCH.equalsIgnoreCase("develop") || !appVersion.isStable();
2525

2626
public static boolean isDockerContainer() {
2727
return false;
@@ -70,7 +70,7 @@ public boolean isSemVerUpdateAvailable(Version currentVersion) {
7070
logger.debug("found version {} from github API. Current version is {}", latestVersion, currentVersion);
7171

7272

73-
if (currentVersion.compareTo(latestVersion) > 0) {
73+
if (currentVersion.isOlderThan(latestVersion)) {
7474
var url = releases.get(0).get("html_url");
7575
logger.warn("An update is available! Version v{} can be downloaded at {}. Out of date versions are not supported.", latestVersion.asVersionString(), url);
7676
return true;

src/main/java/com/github/khakers/modmailviewer/Version.java

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ public Version(@NotNull String prerelease) {
2828

2929
}
3030

31+
private static boolean isDigitString(String str) {
32+
for (char c : str.toCharArray()) {
33+
if (!Character.isDigit(c)) {
34+
return false;
35+
}
36+
}
37+
return true;
38+
}
39+
3140
public String asVersionString() {
3241
var string = new StringBuilder();
3342
string
@@ -47,8 +56,8 @@ public String asVersionString() {
4756
}
4857

4958
/**
50-
* Compares this version to the given version.
51-
* Versions that are the same return false.
59+
* Compares this version to the given version.
60+
* Versions that are the same return false.
5261
*
5362
* @param version the version to compare to
5463
* @return true if this version is newer than the given version
@@ -72,6 +81,16 @@ public boolean isStable() {
7281
return this.prerelease == null;
7382
}
7483

84+
/**
85+
* Compares this version to the given version.
86+
* Versions that are exactly the same return true.
87+
* Versions that are equal for the purposes of semver may return false.
88+
* <p>
89+
* This method takes into account build Metadata.
90+
*
91+
* @param o the object to compare to
92+
* @return true if the object is a version and is equal to this version
93+
*/
7594
@Override
7695
public boolean equals(Object o) {
7796
if (this == o) return true;
@@ -98,9 +117,11 @@ public String toString() {
98117
}
99118

100119
/**
101-
* Calcualtes the difference between two versions.
102-
*
120+
* Calculates the difference between two versions.
121+
* <p>
103122
* PRERELEASE versions are compared lexicographically.
123+
* Per semver, a prerelease version is always less than a stable version of the same major.minor.patch
124+
*
104125
* <p>
105126
* Compares this object with the specified object for order. Returns a
106127
* negative integer, zero, or a positive integer as this object is less
@@ -120,7 +141,7 @@ public String toString() {
120141
* x.compareTo(y)==0} implies that {@code signum(x.compareTo(z))
121142
* == signum(y.compareTo(z))}, for all {@code z}.
122143
*
123-
* @param o the object to be compared.
144+
* @param version the object to be compared.
124145
* @return a negative integer, zero, or a positive integer as this object
125146
* is less than, equal to, or greater than the specified object.
126147
* @throws NullPointerException if the specified object is null
@@ -135,6 +156,36 @@ public String toString() {
135156
*/
136157
@Override
137158
public int compareTo(@NotNull Version version) {
159+
/*
160+
https://semver.org/#spec-item-11
161+
162+
Precedence refers to how versions are compared to each other when ordered.
163+
164+
Precedence MUST be calculated by separating the version into major, minor, patch and pre-release identifiers
165+
in that order (Build metadata does not figure into precedence).
166+
167+
Precedence is determined by the first difference when comparing each of these identifiers from left to right
168+
as follows: Major, minor, and patch versions are always compared numerically.
169+
170+
Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1.
171+
172+
When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version:
173+
174+
Example: 1.0.0-alpha < 1.0.0.
175+
176+
Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined by
177+
comparing each dot separated identifier from left to right until a difference is found as follows:
178+
179+
Identifiers consisting of only digits are compared numerically.
180+
181+
Identifiers with letters or hyphens are compared lexically in ASCII sort order.
182+
183+
Numeric identifiers always have lower precedence than non-numeric identifiers.
184+
185+
A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
186+
187+
Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
188+
*/
138189

139190
if (this.equals(version))
140191
return 0;
@@ -160,14 +211,54 @@ else if (this.patch < version.patch) {
160211
// A prerelease version is always less than a stable version of the same major.minor.patch
161212
if (this.isStable() && !version.isStable()) {
162213
return 1;
163-
} else if (!this.isStable()&& version.isStable()) {
214+
} else if (!this.isStable() && version.isStable()) {
164215
return -1;
165216
}
166217

167-
168218
if (this.prerelease != null && version.prerelease != null) {
219+
/*
220+
Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined
221+
by comparing each dot separated identifier from left to right until a difference is found as follows:
222+
223+
Identifiers consisting of only digits are compared numerically.
224+
225+
Identifiers with letters or hyphens are compared lexically in ASCII sort order.
226+
227+
Numeric identifiers always have lower precedence than non-numeric identifiers.
228+
229+
A larger set of pre-release fields has a higher precedence than a smaller set,
230+
if all of the preceding identifiers are equal.
231+
*/
232+
233+
// separate prerelease identifiers by dot from left to right
234+
var thisPrereleaseIdentifiers = this.prerelease.split("\\.");
235+
var versionPrereleaseIdentifiers = version.prerelease.split("\\.");
236+
for (int i = 0; i < thisPrereleaseIdentifiers.length; i++) {
237+
if (i >= versionPrereleaseIdentifiers.length) {
238+
// this prerelease version has more identifiers than the other prerelease version
239+
return 1;
240+
}
241+
242+
// Identifiers consisting of only digits are compared numerically.
243+
if (isDigitString(thisPrereleaseIdentifiers[i]) && isDigitString(versionPrereleaseIdentifiers[i])) {
244+
var result = Integer.compare(
245+
Integer.parseInt(thisPrereleaseIdentifiers[i]),
246+
Integer.parseInt(versionPrereleaseIdentifiers[i]));
247+
if (result != 0) {
248+
return result;
249+
}
250+
}
251+
252+
// Identifiers with letters or hyphens are compared lexically in ASCII sort order.
253+
var result = thisPrereleaseIdentifiers[i].compareTo(versionPrereleaseIdentifiers[i]);
254+
if (result != 0) {
255+
return result;
256+
}
257+
}
258+
259+
169260
// return reverse comparison because prerelease versions are compared lexicographically
170-
return -version.prerelease.compareTo(this.prerelease);
261+
// return -version.prerelease.compareTo(this.prerelease);
171262
}
172263

173264
return 0;

src/test/java/com/github/khakers/modmailviewer/VersionTest.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
import org.junit.jupiter.api.Test;
55
import org.junit.jupiter.params.ParameterizedTest;
66
import org.junit.jupiter.params.provider.CsvFileSource;
7+
import org.junit.jupiter.params.provider.CsvSource;
78

89
class VersionTest {
910

1011
@ParameterizedTest()
1112
@CsvFileSource(resources = "/validSemver.csv")
12-
void version_ShouldParseAllValidSemver(String input){
13+
void version_ShouldParseAllValidSemver(String input) {
1314
System.out.println(new Version(input));
1415
}
16+
1517
@ParameterizedTest()
1618
@CsvFileSource(resources = "/invalidSemver.csv")
1719
void version_ShouldNotParseInvalidSemver(String input) {
@@ -68,20 +70,23 @@ void version_NewerPrereleaseIsNewer() {
6870
// newer version should be greater than old version
6971
Assertions.assertTrue(newerPrerelease.isNewerThan(olderPrerelease), "Newer pre-release version should be greater than older prerelease version");
7072
}
73+
7174
@Test
7275
void version_OlderPrereleaseIsOlder() {
7376
var oldVersion = new Version("1.0.0-alpha.1");
7477
var newerVersion = new Version("1.0.0-alpha.2");
7578
// newer version should be greater than old version
7679
Assertions.assertTrue(oldVersion.isOlderThan(newerVersion), "Newer pre-release version should be greater than older prerelease version");
7780
}
81+
7882
@Test
7983
void version_IdenticalPrereleaseVersionsAreSame() {
8084
var version = new Version("1.0.0-alpha.1");
8185
var version1 = new Version("1.0.0-alpha.1");
8286
Assertions.assertFalse(version.isOlderThan(version1));
8387
Assertions.assertFalse(version.isNewerThan(version1));
8488
}
89+
8590
@Test
8691
void version_StableGreaterThanPrerelease() {
8792
var stable = new Version("1.0.0");
@@ -97,4 +102,22 @@ void version_PrereleaseLessThanStable() {
97102
// prerelease should be less than stable
98103
Assertions.assertTrue(prerelease.isOlderThan(stable), "Pre-release should be older than stable of the same version");
99104
}
105+
106+
//1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
107+
@ParameterizedTest
108+
@CsvSource({
109+
"1.0.0-alpha,1.0.0-alpha.1",
110+
"1.0.0-alpha.1,1.0.0-alpha.beta",
111+
"1.0.0-alpha.beta,1.0.0-beta",
112+
"1.0.0-beta,1.0.0-beta.2",
113+
"1.0.0-beta.2,1.0.0-beta.11",
114+
"1.0.0-beta.11,1.0.0-rc.1",
115+
"1.0.0-rc.1,1.0.0",
116+
"1.2.3-rc,1.2.3-2"})
117+
void version_PrereleasePrecedenceMatrix(String olderVersionString, String newerVersionString) {
118+
var olderVersion = new Version(olderVersionString);
119+
var newerVersion = new Version(newerVersionString);
120+
Assertions.assertTrue(newerVersion.isNewerThan(olderVersion));
121+
Assertions.assertFalse(newerVersion.isOlderThan(olderVersion));
122+
}
100123
}

0 commit comments

Comments
 (0)