diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/comparator/VersionComparator.java b/hutool-core/src/main/java/org/dromara/hutool/core/comparator/VersionComparator.java index 7fa6b29d9..f02385ae0 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/comparator/VersionComparator.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/comparator/VersionComparator.java @@ -12,22 +12,16 @@ package org.dromara.hutool.core.comparator; -import org.dromara.hutool.core.convert.Convert; -import org.dromara.hutool.core.math.NumberUtil; -import org.dromara.hutool.core.regex.ReUtil; -import org.dromara.hutool.core.text.StrUtil; -import org.dromara.hutool.core.text.split.SplitUtil; +import org.dromara.hutool.core.lang.Version; import java.io.Serializable; -import java.util.List; -import java.util.regex.Pattern; /** * 版本比较器
* 比较两个版本的大小
* 排序时版本从小到大排序,即比较时小版本在前,大版本在后
* 支持如:1.3.20.8,6.82.20160101,8.5a/8.5c等版本形式
- * 参考:https://www.cnblogs.com/shihaiming/p/6286575.html + * 参考:java.lang.module.ModuleDescriptor.Version * * @author Looly * @since 4.0.2 @@ -35,8 +29,6 @@ import java.util.regex.Pattern; public class VersionComparator extends NullComparator implements Serializable { private static final long serialVersionUID = 1L; - private static final Pattern PATTERN_PRE_NUMBERS= Pattern.compile("^\\d+"); - /** * 单例 */ @@ -78,40 +70,6 @@ public class VersionComparator extends NullComparator implements Seriali * @param version2 版本2 */ private static int compareVersion(final String version1, final String version2) { - final List v1s = SplitUtil.splitTrim(version1, StrUtil.DOT); - final List v2s = SplitUtil.splitTrim(version2, StrUtil.DOT); - - int diff = 0; - final int minSize = Math.min(v1s.size(), v2s.size());// 取最小长度值 - String v1; - String v2; - for (int i = 0; i < minSize; i++) { - v1 = v1s.get(i); - v2 = v2s.get(i); - // 先比较长度 - diff = v1.length() - v2.length(); - if (0 == diff) { - // 长度相同,直接比较字符或数字 - diff = v1.compareTo(v2); - } else { - // 不同长度,且含有字母 - if(!NumberUtil.isNumber(v1) || !NumberUtil.isNumber(v2)){ - //不同长度的先比较前面的数字;前面数字不相等时,按数字大小比较;数字相等的时候,继续按长度比较,类似于 103 > 102a - final int v1Num = Convert.toInt(ReUtil.get(PATTERN_PRE_NUMBERS, v1, 0), 0); - final int v2Num = Convert.toInt(ReUtil.get(PATTERN_PRE_NUMBERS, v2, 0), 0); - final int diff1 = v1Num - v2Num; - if (diff1 != 0) { - diff = diff1; - } - } - } - if (diff != 0) { - //已有结果,结束 - break; - } - } - - // 如果已经分出大小,则直接返回,如果未分出大小,则再比较位数,有子版本的为大; - return (diff != 0) ? diff : v1s.size() - v2s.size(); + return CompareUtil.compare(Version.of(version1), Version.of(version2)); } } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/lang/Version.java b/hutool-core/src/main/java/org/dromara/hutool/core/lang/Version.java new file mode 100644 index 000000000..1d60c9dbe --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/lang/Version.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2024. looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.lang; + +import org.dromara.hutool.core.comparator.CompareUtil; +import org.dromara.hutool.core.text.CharUtil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 字符串版本表示,用于解析版本号的不同部分并比较大小。
+ * 来自:java.lang.module.ModuleDescriptor.Version + * + * @author Looly + */ +public class Version implements Comparable, Serializable { + private static final long serialVersionUID = 1L; + + /** + * 解析版本字符串为Version对象 + * + * @param v 版本字符串 + * @return The resulting {@code Version} + * @throws IllegalArgumentException 如果 {@code v} 为 {@code null}或 ""或无法解析的字符串,抛出此异常 + */ + public static Version of(final String v) { + return new Version(v); + } + + private final String version; + + private final List sequence; + private final List pre; + private final List build; + + /** + * 版本对象,格式:tok+ ( '-' tok+)? ( '+' tok+)?,版本之间使用'.'或'-'分隔,版本号可能包含'+'
+ * 数字部分按照大小比较,字符串按照字典顺序比较。 + * + *
    + *
  1. sequence: 主版本号
  2. + *
  3. pre: 次版本号
  4. + *
  5. build: 构建版本
  6. + *
+ * + * @param v 版本字符串 + */ + public Version(final String v) { + Assert.notNull(v, "Null version string"); + final int n = v.length(); + if (n == 0){ + throw new IllegalArgumentException("Empty version string"); + } + this.version = v; + this.sequence = new ArrayList<>(4); + this.pre = new ArrayList<>(2); + this.build = new ArrayList<>(2); + + int i = 0; + char c = v.charAt(i); + // 不检查开头字符为数字,字母按照字典顺序的数字对待 + + final List sequence = this.sequence; + final List pre = this.pre; + final List build = this.build; + + // 解析主版本 + i = takeNumber(v, i, sequence); + + while (i < n) { + c = v.charAt(i); + if (c == '.') { + i++; + continue; + } + if (c == '-' || c == '+') { + i++; + break; + } + if (CharUtil.isNumber(c)){ + i = takeNumber(v, i, sequence); + }else{ + i = takeString(v, i, sequence); + } + } + + if (c == '-' && i >= n){ + return; + } + + // 解析次版本 + while (i < n) { + c = v.charAt(i); + if (c >= '0' && c <= '9') + i = takeNumber(v, i, pre); + else + i = takeString(v, i, pre); + if (i >= n){ + break; + } + c = v.charAt(i); + if (c == '.' || c == '-') { + i++; + continue; + } + if (c == '+') { + i++; + break; + } + } + + if (c == '+' && i >= n){ + return; + } + + // 解析build版本 + while (i < n) { + c = v.charAt(i); + if (c >= '0' && c <= '9') { + i = takeNumber(v, i, build); + }else { + i = takeString(v, i, build); + } + if (i >= n){ + break; + } + c = v.charAt(i); + if (c == '.' || c == '-' || c == '+') { + i++; + } + } + } + + @Override + public int compareTo(final Version that) { + int c = compareTokens(this.sequence, that.sequence); + if (c != 0) { + return c; + } + if (this.pre.isEmpty()) { + if (!that.pre.isEmpty()) { + return +1; + } + } else { + if (that.pre.isEmpty()) { + return -1; + } + } + c = compareTokens(this.pre, that.pre); + if (c != 0) { + return c; + } + return compareTokens(this.build, that.build); + } + + @Override + public boolean equals(final Object ob) { + if (!(ob instanceof Version)){ + return false; + } + return compareTo((Version) ob) == 0; + } + + @Override + public int hashCode() { + return version.hashCode(); + } + + @Override + public String toString() { + return version; + } + + // region ----- private methods + /** + * 获取字符串中从位置i开始的数字,并加入到acc中
+ * 如 a123b,则从1开始,解析到acc中为[1, 2, 3] + * + * @param s 字符串 + * @param i 位置 + * @param acc 数字列表 + * @return 结束位置(不包含) + */ + private static int takeNumber(final String s, int i, final List acc) { + char c = s.charAt(i); + int d = (c - '0'); + final int n = s.length(); + while (++i < n) { + c = s.charAt(i); + if (CharUtil.isNumber(c)) { + d = d * 10 + (c - '0'); + continue; + } + break; + } + acc.add(d); + return i; + } + + // Take a string token starting at position i + // Append it to the given list + // Return the index of the first character not taken + // Requires: s.charAt(i) is not '.' + // + + /** + * 获取字符串中从位置i开始的字符串,并加入到acc中
+ * 字符串结束的位置为'.'、'-'、'+'和数字 + * + * @param s 版本字符串 + * @param i 开始位置 + * @param acc 字符串列表 + * @return 结束位置(不包含) + */ + private static int takeString(final String s, int i, final List acc) { + final int b = i; + final int n = s.length(); + while (++i < n) { + final char c = s.charAt(i); + if (c != '.' && c != '-' && c != '+' && !(c >= '0' && c <= '9')){ + continue; + } + break; + } + acc.add(s.substring(b, i)); + return i; + } + + /** + * 比较节点 + * @param ts1 节点1 + * @param ts2 节点2 + * @return 比较结果 + */ + private int compareTokens(final List ts1, final List ts2) { + final int n = Math.min(ts1.size(), ts2.size()); + for (int i = 0; i < n; i++) { + final Object o1 = ts1.get(i); + final Object o2 = ts2.get(i); + if ((o1 instanceof Integer && o2 instanceof Integer) + || (o1 instanceof String && o2 instanceof String)) { + final int c = CompareUtil.compare(o1, o2, null); + if (c == 0){ + continue; + } + return c; + } + // Types differ, so convert number to string form + final int c = o1.toString().compareTo(o2.toString()); + if (c == 0){ + continue; + } + return c; + } + final List rest = ts1.size() > ts2.size() ? ts1 : ts2; + final int e = rest.size(); + for (int i = n; i < e; i++) { + final Object o = rest.get(i); + if (o instanceof Integer && ((Integer) o) == 0){ + continue; + } + return ts1.size() - ts2.size(); + } + return 0; + } + // endregion +} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/comparator/VersionComparatorTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/comparator/VersionComparatorTest.java index 5f7bd126c..0d3d8e5f1 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/comparator/VersionComparatorTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/comparator/VersionComparatorTest.java @@ -98,4 +98,27 @@ public class VersionComparatorTest { compare = VersionComparator.INSTANCE.compare("1.12.1c", "1.12.2"); Assertions.assertTrue(compare < 0); } + + @Test + void equalsTest2() { + final int compare = VersionComparator.INSTANCE.compare("1.12.0", "1.12"); + Assertions.assertEquals(0, compare); + } + + @Test + void I8Z3VETest() { + // 传递性测试 + int compare = VersionComparator.INSTANCE.compare("260", "a-34"); + Assertions.assertTrue(compare > 0); + compare = VersionComparator.INSTANCE.compare("a-34", "a-3"); + Assertions.assertTrue(compare > 0); + compare = VersionComparator.INSTANCE.compare("260", "a-3"); + Assertions.assertTrue(compare > 0); + } + + @Test + void startWithNoneNumberTest() { + final int compare = VersionComparator.INSTANCE.compare("V1", "A1"); + Assertions.assertTrue(compare > 0); + } }