From 058c801442976ad3e400c154b34d86e23a176f4d Mon Sep 17 00:00:00 2001 From: Looly Date: Sat, 10 Feb 2024 12:37:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DVersionComparator=E8=BF=9D?= =?UTF-8?q?=E5=8F=8D=E4=BC=A0=E9=80=92=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 + .../core/comparator/VersionComparator.java | 40 +-- .../java/cn/hutool/core/lang/Version.java | 279 ++++++++++++++++++ .../comparator/VersionComparatorTest.java | 23 ++ 4 files changed, 307 insertions(+), 37 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Version.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b46bdb859..2cb49d66c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### 🐣新特性 * 【db 】 RedisDS增加user支持(issue#I8XEQ4@Gitee) * 【core 】 MapUtil增加partition方法(pr#1170@Gitee) +* 【core 】 增加Version类(issue#I8Z3VE@Gitee) ### 🐞Bug修复 * 【crypto】 修复BouncyCastleProvider导致graalvm应用报错UnsupportedFeatureError(pr#3464@Github) @@ -17,6 +18,7 @@ * 【core 】 修复CollUtil.containsAll在coll2长度大于coll1时逻辑歧义问题(issue#I8Z2Q4@Gitee) * 【poi 】 修复当sheetName 不存在时,ExcelUtil.getReader方法不会释放文件问题(issue#I8ZIQC@Gitee) * 【crypto】 通过添加系统属性hutool.crypto.decodeHex强制关闭hex识别以解决hex和Base64歧义问题(issue#I90M9D@Gitee) +* 【core 】 修复VersionComparator违反传递问题(issue#I8Z3VE@Gitee) ------------------------------------------------------------------------------------------------------------- # 5.8.25(2024-01-11) diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java index 0e12a8389..962b1b33f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java @@ -1,6 +1,7 @@ package cn.hutool.core.comparator; import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Version; import cn.hutool.core.util.*; import java.io.Serializable; @@ -13,7 +14,7 @@ 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 @@ -21,8 +22,6 @@ import java.util.regex.Pattern; public class VersionComparator implements Comparator, Serializable { private static final long serialVersionUID = 8083701245147495562L; - private static final Pattern PATTERN_PRE_NUMBERS= Pattern.compile("^\\d+"); - /** 单例 */ public static final VersionComparator INSTANCE = new VersionComparator(); @@ -64,39 +63,6 @@ public class VersionComparator implements Comparator, Serializable { return 1; } - final List v1s = StrUtil.split(version1, CharUtil.DOT); - final List v2s = StrUtil.split(version2, CharUtil.DOT); - - int diff = 0; - int minLength = Math.min(v1s.size(), v2s.size());// 取最小长度值 - String v1; - String v2; - for (int i = 0; i < minLength; 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/cn/hutool/core/lang/Version.java b/hutool-core/src/main/java/cn/hutool/core/lang/Version.java new file mode 100644 index 000000000..1075bf466 --- /dev/null +++ b/hutool-core/src/main/java/cn/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 cn.hutool.core.lang; + +import cn.hutool.core.comparator.CompareUtil; +import cn.hutool.core.util.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/cn/hutool/core/comparator/VersionComparatorTest.java b/hutool-core/src/test/java/cn/hutool/core/comparator/VersionComparatorTest.java index 0f7c27220..a8afe0cf7 100644 --- a/hutool-core/src/test/java/cn/hutool/core/comparator/VersionComparatorTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/comparator/VersionComparatorTest.java @@ -88,4 +88,27 @@ public class VersionComparatorTest { compare = VersionComparator.INSTANCE.compare("1.12.1c", "1.12.2"); Assert.assertTrue(compare < 0); } + + @Test + public void equalsTest2() { + final int compare = VersionComparator.INSTANCE.compare("1.12.0", "1.12"); + Assert.assertEquals(0, compare); + } + + @Test + public void I8Z3VETest() { + // 传递性测试 + int compare = VersionComparator.INSTANCE.compare("260", "a-34"); + Assert.assertTrue(compare > 0); + compare = VersionComparator.INSTANCE.compare("a-34", "a-3"); + Assert.assertTrue(compare > 0); + compare = VersionComparator.INSTANCE.compare("260", "a-3"); + Assert.assertTrue(compare > 0); + } + + @Test + public void startWithNoneNumberTest() { + final int compare = VersionComparator.INSTANCE.compare("V1", "A1"); + Assert.assertTrue(compare > 0); + } }