修复VersionComparator违反传递问题

This commit is contained in:
Looly 2024-02-10 12:37:30 +08:00
parent 115b15f010
commit 1fba2ca52b
3 changed files with 305 additions and 45 deletions

View File

@ -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;
/**
* 版本比较器<br>
* 比较两个版本的大小<br>
* 排序时版本从小到大排序即比较时小版本在前大版本在后<br>
* 支持如1.3.20.86.82.201601018.5a/8.5c等版本形式<br>
* 参考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<String> 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<String> implements Seriali
* @param version2 版本2
*/
private static int compareVersion(final String version1, final String version2) {
final List<String> v1s = SplitUtil.splitTrim(version1, StrUtil.DOT);
final List<String> 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));
}
}

View File

@ -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;
/**
* 字符串版本表示用于解析版本号的不同部分并比较大小<br>
* 来自java.lang.module.ModuleDescriptor.Version
*
* @author Looly
*/
public class Version implements Comparable<Version>, 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<Object> sequence;
private final List<Object> pre;
private final List<Object> build;
/**
* 版本对象格式tok+ ( '-' tok+)? ( '+' tok+)?版本之间使用'.''-'分隔版本号可能包含'+'<br>
* 数字部分按照大小比较字符串按照字典顺序比较
*
* <ol>
* <li>sequence: 主版本号</li>
* <li>pre: 次版本号</li>
* <li>build: 构建版本</li>
* </ol>
*
* @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<Object> sequence = this.sequence;
final List<Object> pre = this.pre;
final List<Object> 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中<br>
* a123b则从1开始解析到acc中为[1, 2, 3]
*
* @param s 字符串
* @param i 位置
* @param acc 数字列表
* @return 结束位置不包含
*/
private static int takeNumber(final String s, int i, final List<Object> 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中<br>
* 字符串结束的位置为'.''-''+'和数字
*
* @param s 版本字符串
* @param i 开始位置
* @param acc 字符串列表
* @return 结束位置不包含
*/
private static int takeString(final String s, int i, final List<Object> 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<Object> ts1, final List<Object> 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<Object> 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
}

View File

@ -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);
}
}