diff --git a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java index 5206404cf107..d90c1bf3fd4e 100644 --- a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java +++ b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java @@ -2841,6 +2841,29 @@ public class StringUtil extends StringUtilRt { return ignoreCase ? s1.compareToIgnoreCase(s2) : s1.compareTo(s2); } + @Contract(pure = true) + public static int compare(@Nullable CharSequence s1, @Nullable CharSequence s2, boolean ignoreCase) { + if (s1 == s2) return 0; + if (s1 == null) return -1; + if (s2 == null) return 1; + + int length1 = s1.length(); + int length2 = s2.length(); + int i = 0; + int j = 0; + for (; i < length1 && j < length2; i++, j++) { + int diff = compare(s1.charAt(i), s2.charAt(j), ignoreCase); + if (diff != 0) { + return diff; + } + } + if (i < length1) return +1; + if (j < length2) return -1; + if (length1 != length2) return length1 - length2; + + return ignoreCase ? compare(s1, s2, false) : 0; + } + @Contract(pure = true) public static int comparePairs(@Nullable String s1, @Nullable String t1, @Nullable String s2, @Nullable String t2, boolean ignoreCase) { final int compare = compare(s1, s2, ignoreCase); @@ -3399,4 +3422,4 @@ public class StringUtil extends StringUtilRt { } return true; } -} \ No newline at end of file +} diff --git a/platform/util/src/com/intellij/util/text/SemVer.java b/platform/util/src/com/intellij/util/text/SemVer.java index 28188fd7cc1f..5893cfe06088 100644 --- a/platform/util/src/com/intellij/util/text/SemVer.java +++ b/platform/util/src/com/intellij/util/text/SemVer.java @@ -19,23 +19,34 @@ import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Objects; + /** * Holds Semantic Version. */ public final class SemVer implements Comparable { - /** @deprecated */ + /** + * @deprecated + */ @Deprecated public static final SemVer UNKNOWN = new SemVer("?", 0, 0, 0); private final String myRawVersion; private final int myMajor; private final int myMinor; private final int myPatch; + @Nullable + private final String myPreRelease; public SemVer(@NotNull String rawVersion, int major, int minor, int patch) { + this(rawVersion, major, minor, patch, null); + } + + public SemVer(@NotNull String rawVersion, int major, int minor, int patch, @Nullable String preRelease) { myRawVersion = rawVersion; myMajor = major; myMinor = minor; myPatch = patch; + myPreRelease = preRelease; } @NotNull @@ -55,9 +66,14 @@ public final class SemVer implements Comparable { return myPatch; } + @Nullable + public String getPreRelease() { + return myPreRelease; + } + @NotNull public String getParsedVersion() { - return myMajor + "." + myMinor + "." + myPatch; + return myMajor + "." + myMinor + "." + myPatch + (myPreRelease != null ? "-" + myPreRelease : ""); } @Override @@ -68,7 +84,10 @@ public final class SemVer implements Comparable { diff = myMinor - other.myMinor; if (diff != 0) return diff; - return myPatch - other.myPatch; + diff = myPatch - other.myPatch; + if (diff != 0) return diff; + + return comparePrerelease(myPreRelease, other.myPreRelease); } public boolean isGreaterOrEqualThan(int major, int minor, int patch) { @@ -83,7 +102,10 @@ public final class SemVer implements Comparable { if (o == null || getClass() != o.getClass()) return false; SemVer semVer = (SemVer)o; - return myMajor == semVer.myMajor && myMinor == semVer.myMinor && myPatch == semVer.myPatch; + return myMajor == semVer.myMajor + && myMinor == semVer.myMinor + && myPatch == semVer.myPatch + && Objects.equals(myPreRelease, semVer.myPreRelease); } @Override @@ -91,6 +113,9 @@ public final class SemVer implements Comparable { int result = myMajor; result = 31 * result + myMinor; result = 31 * result + myPatch; + if (myPreRelease != null) { + result = 31 * result + myPreRelease.hashCode(); + } return result; } @@ -99,6 +124,83 @@ public final class SemVer implements Comparable { return myRawVersion; } + private static int comparePrerelease(@Nullable String pre1, @Nullable String pre2) { + if (pre1 == null) { + return pre2 == null ? 0 : 1; + } + else if (pre2 == null) { + return -1; + } + int length1 = pre1.length(); + int length2 = pre2.length(); + + if (length1 == length2 && pre1.equals(pre2)) return 0; + + int start1 = 0; + int start2 = 0; + int diff; + + // compare each segment separately + do { + int end1 = pre1.indexOf('.', start1); + int end2 = pre2.indexOf('.', start2); + + if (end1 < 0) end1 = length1; + if (end2 < 0) end2 = length2; + + + CharSequence segment1 = new CharSequenceSubSequence(pre1, start1, end1); + CharSequence segment2 = new CharSequenceSubSequence(pre2, start2, end2); + if (isNumeric(segment1)) { + if (!isNumeric(segment2)) { + return -1; + } + diff = compareNumeric(segment1, segment2); + } + else if (isNumeric(segment2)) { + return 1; + } + else { + diff = StringUtil.compare(segment1, segment2, false); + } + start1 = end1 + 1; + start2 = end2 + 1; + } + while (diff == 0 && start1 < length1 && start2 < length2); + + if (diff != 0) return diff; + if (start1 >= length1) { + if (start2 >= length2) { + return 0; + } + return -1; + } + else { + return 1; + } + } + + private static int compareNumeric(CharSequence segment1, CharSequence segment2) { + int length1 = segment1.length(); + int length2 = segment2.length(); + int diff = Integer.compare(length1, length2); + for (int i = 0; i <= length1 && diff == 0; i++) { + diff = segment1.charAt(i) - segment2.charAt(i); + } + return diff; + } + + private static boolean isNumeric(CharSequence segment) { + int length = segment.length(); + for (int i = 0; i < length; i++) { + int ch = segment.charAt(i); + if (ch < '0' || ch > '9') { + return false; + } + } + return true; + } + @Nullable public static SemVer parseFromText(@Nullable String text) { if (text != null) { @@ -106,14 +208,16 @@ public final class SemVer implements Comparable { if (majorEndIdx >= 0) { int minorEndIdx = text.indexOf('.', majorEndIdx + 1); if (minorEndIdx >= 0) { - int patchEndIdx = text.indexOf('-', minorEndIdx + 1); - if (patchEndIdx < 0) patchEndIdx = text.length(); + int preReleaseIdx = text.indexOf('-', minorEndIdx + 1); + int patchEndIdx = preReleaseIdx >= 0 ? preReleaseIdx : text.length(); int major = StringUtil.parseInt(text.substring(0, majorEndIdx), -1); int minor = StringUtil.parseInt(text.substring(majorEndIdx + 1, minorEndIdx), -1); int patch = StringUtil.parseInt(text.substring(minorEndIdx + 1, patchEndIdx), -1); + String preRelease = preReleaseIdx >= 0 ? text.substring(preReleaseIdx + 1) : null; + if (major >= 0 && minor >= 0 && patch >= 0) { - return new SemVer(text, major, minor, patch); + return new SemVer(text, major, minor, patch, preRelease); } } } diff --git a/platform/util/testSrc/com/intellij/util/text/SemVerTest.java b/platform/util/testSrc/com/intellij/util/text/SemVerTest.java index 0f6eafb31fa7..395ce47c6c9c 100644 --- a/platform/util/testSrc/com/intellij/util/text/SemVerTest.java +++ b/platform/util/testSrc/com/intellij/util/text/SemVerTest.java @@ -15,6 +15,7 @@ */ package com.intellij.util.text; +import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.junit.Test; @@ -23,15 +24,15 @@ import static org.assertj.core.api.Assertions.assertThat; public class SemVerTest { @Test public void parsing() { - assertParsed("0.9.2", 0, 9, 2); - assertParsed("0.9.2-", 0, 9, 2); - assertParsed("0.9.2-dart", 0, 9, 2); - assertParsed("4.0.0-alpha.1", 4, 0, 0); - assertParsed("0.10.0-rc-1", 0, 10, 0); - assertParsed("1.0.0-rc-1", 1, 0, 0); - assertParsed("1.0.0-alpha", 1, 0, 0); - assertParsed("1.0.0-0.3.7", 1, 0, 0); - assertParsed("1.0.0-x.7.z.92", 1, 0, 0); + assertParsed("0.9.2", 0, 9, 2, null); + assertParsed("0.9.2-", 0, 9, 2, ""); + assertParsed("0.9.2-dart", 0, 9, 2, "dart"); + assertParsed("4.0.0-alpha.1", 4, 0, 0, "alpha.1"); + assertParsed("0.10.0-rc-1", 0, 10, 0, "rc-1"); + assertParsed("1.0.0-rc-1", 1, 0, 0, "rc-1"); + assertParsed("1.0.0-alpha", 1, 0, 0, "alpha"); + assertParsed("1.0.0-0.3.7", 1, 0, 0, "0.3.7"); + assertParsed("1.0.0-x.7.z.92", 1, 0, 0, "x.7.z.92"); assertNotParsed(null); assertNotParsed(""); @@ -42,31 +43,60 @@ public class SemVerTest { @Test public void comparing() { - assertThat(parse("1.0.0")).isGreaterThan(parse("0.10.0")); - assertThat(parse("1.0.0")).isLessThan(parse("2.10.0")); - - assertThat(parse("0.30.0")).isGreaterThan(parse("0.5.1000")); - assertThat(parse("0.30.10")).isLessThan(parse("0.100.0")); - - assertThat(parse("2.9.123-test")).isGreaterThan(parse("2.9.100")); - assertThat(parse("2.9.123-test")).isLessThan(parse("2.9.124")); - assertThat(parse("11.123.0")).isEqualTo(parse("11.123.0")); assertThat(parse("11.123.0")).isEqualByComparingTo(parse("11.123.0")); + assertThat(parse("11.123.0-a.b.c-1")).isEqualTo(parse("11.123.0-a.b.c-1")); + assertThat(parse("11.123.0-a.b.c-1")).isEqualByComparingTo(parse("11.123.0-a.b.c-1")); + + assertPrecedence("0.10.0", "1.0.0"); + assertPrecedence("1.0.0","2.10.0"); + assertPrecedence("0.5.1000", "0.30.0"); + assertPrecedence("0.30.10","0.100.0"); + + assertPrecedence("2.9.100", "2.9.123-test"); + assertPrecedence("2.9.123-test","2.9.124"); + assertPrecedence("2.9.123","2.9.124-test"); + + assertPrecedence("1.2.3-a","1.2.3"); + + assertPrecedence("1.2.3-12","1.2.3-a"); + assertPrecedence("1.2.3-22","1.2.3-100"); + assertPrecedence("1.2.3-22","1.2.3-31"); + + assertPrecedence("1.2.3-a.b.c","1.2.3-a.b.d"); + assertPrecedence("1.2.3-a.b.c","1.2.3-a.b.c.a"); + + assertPrecedence("1.2.3-a.b.1","1.2.3-a.b.c"); + assertPrecedence("1.2.3-a.b.1","1.2.3-a.c.1"); + assertPrecedence("1.2.3-a.cbc.100","1.2.3-a.cca.1"); + assertPrecedence("1.2.3-a.cb.1","1.2.3-a.cba.1"); + Assert.assertTrue(parse("4.12.5").isGreaterOrEqualThan(4, 12, 5)); + Assert.assertTrue(parse("4.12.5-a").isGreaterOrEqualThan(4, 12, 5)); Assert.assertTrue(parse("4.12.5").isGreaterOrEqualThan(4, 12, 4)); + Assert.assertTrue(parse("4.12.5-a").isGreaterOrEqualThan(4, 12, 4)); Assert.assertTrue(parse("4.12.5").isGreaterOrEqualThan(4, 11, 0)); Assert.assertTrue(parse("4.12.5").isGreaterOrEqualThan(4, 11, 9)); Assert.assertTrue(parse("4.12.5").isGreaterOrEqualThan(3, 100, 100)); Assert.assertFalse(parse("4.12.5").isGreaterOrEqualThan(4, 12, 6)); + Assert.assertFalse(parse("4.12.5-a").isGreaterOrEqualThan(4, 12, 6)); Assert.assertFalse(parse("4.12.5").isGreaterOrEqualThan(4, 13, 0)); Assert.assertFalse(parse("4.12.5").isGreaterOrEqualThan(5, 1, 0)); } - private static void assertParsed(String version, int expectedMajor, int expectedMinor, int expectedPatch) { - assertThat(parse(version)).isEqualTo(new SemVer(version, expectedMajor, expectedMinor, expectedPatch)); + private static void assertPrecedence(String lesserVersion, String higherVersion) { + SemVer v1 = parse(lesserVersion); + SemVer v2 = parse(higherVersion); + assertThat(v1).isLessThan(v2); + assertThat(v2).isGreaterThan(v1); + assertThat(v1).isNotEqualByComparingTo(v2); + assertThat(v1).isNotEqualTo(v2); + } + + private static void assertParsed(String version, int expectedMajor, int expectedMinor, int expectedPatch, @Nullable String expectedPreRelease) { + assertThat(parse(version)).isEqualTo(new SemVer(version, expectedMajor, expectedMinor, expectedPatch, expectedPreRelease)); } private static void assertNotParsed(String version) { @@ -78,4 +108,4 @@ public class SemVerTest { assertThat(semVer).describedAs(text).isNotNull(); return semVer; } -} \ No newline at end of file +}