优化 VersionNumber (#2240)

* 优化 VersionNumber

* Add more tests

* update

* update tests
This commit is contained in:
Glavo
2023-05-24 21:28:22 +08:00
committed by GitHub
parent e7c108d843
commit 49287453eb
2 changed files with 292 additions and 142 deletions

View File

@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.util.versioning;
import org.jackhuang.hmcl.util.StringUtils;
import java.math.BigInteger;
import java.util.*;
@@ -29,7 +27,9 @@ import java.util.*;
* Maybe we can migrate to org.jenkins-ci:version-number:1.7?
* @see <a href="http://maven.apache.org/pom.html#Version_Order_Specification">Specification</a>
*/
public class VersionNumber implements Comparable<VersionNumber> {
public final class VersionNumber implements Comparable<VersionNumber> {
public static final Comparator<String> VERSION_COMPARATOR = Comparator.comparing(VersionNumber::asVersion);
public static VersionNumber asVersion(String version) {
Objects.requireNonNull(version);
@@ -41,68 +41,85 @@ public class VersionNumber implements Comparable<VersionNumber> {
}
public static boolean isIntVersionNumber(String version) {
if (version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9'))
&& !version.contains("..") && StringUtils.isNotBlank(version)) {
String[] arr = version.split("\\.");
for (String str : arr)
if (str.length() > 9)
// Numbers which are larger than 1e9 cannot be stored as integer.
return false;
return true;
} else {
if (version.isEmpty()) {
return false;
}
int idx = 0;
boolean cont = true;
do {
int dotIndex = version.indexOf('.', idx);
if (dotIndex == idx || dotIndex == version.length() - 1) {
return false;
}
int endIndex;
if (dotIndex < 0) {
cont = false;
endIndex = version.length();
} else {
endIndex = dotIndex;
}
if (endIndex - idx > 9)
// Numbers which are larger than 10^10 cannot be stored as integer
return false;
for (int i = idx; i < endIndex; i++) {
char ch = version.charAt(i);
if (ch < '0' || ch > '9')
return false;
}
idx = endIndex + 1;
} while (cont);
return true;
}
private String value;
private String canonical;
private ListItem items;
private interface Item {
int INTEGER_ITEM = 0;
int STRING_ITEM = 1;
int LIST_ITEM = 2;
int LONG_ITEM = 0;
int BIGINTEGER_ITEM = 1;
int STRING_ITEM = 2;
int LIST_ITEM = 3;
int compareTo(Item item);
int getType();
boolean isNull();
void appendTo(StringBuilder buffer);
}
/**
* Represents a numeric item in the version item list.
*/
private static class IntegerItem
implements Item {
private final BigInteger value;
private static final class LongItem implements Item {
private final long value;
public static final IntegerItem ZERO = new IntegerItem();
public static final LongItem ZERO = new LongItem(0L);
private IntegerItem() {
this.value = BigInteger.ZERO;
}
IntegerItem(String str) {
this.value = new BigInteger(str);
LongItem(long value) {
this.value = value;
}
public int getType() {
return INTEGER_ITEM;
return LONG_ITEM;
}
public boolean isNull() {
return BigInteger.ZERO.equals(value);
return value == 0L;
}
public int compareTo(Item item) {
if (item == null) {
return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1
return value == 0L ? 0 : 1; // 1.0 == 1, 1.1 > 1
}
switch (item.getType()) {
case INTEGER_ITEM:
return value.compareTo(((IntegerItem) item).value);
case LONG_ITEM:
long itemValue = ((LongItem) item).value;
return Long.compare(value, itemValue);
case BIGINTEGER_ITEM:
return -1;
case STRING_ITEM:
return 1; // 1.1 > 1-sp
@@ -111,10 +128,68 @@ public class VersionNumber implements Comparable<VersionNumber> {
return 1; // 1.1 > 1-1
default:
throw new RuntimeException("invalid item: " + item.getClass());
throw new AssertionError("invalid item: " + item.getClass());
}
}
@Override
public void appendTo(StringBuilder buffer) {
buffer.append(value);
}
public String toString() {
return Long.toString(value);
}
}
/**
* Represents a numeric item in the version item list.
*/
private static final class BigIntegerItem implements Item {
private final BigInteger value;
BigIntegerItem(String str) {
this.value = new BigInteger(str);
}
public int getType() {
return BIGINTEGER_ITEM;
}
public boolean isNull() {
// Never be 0
// return BigInteger.ZERO.equals(value);
return false;
}
public int compareTo(Item item) {
if (item == null) {
// return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1
return 1;
}
switch (item.getType()) {
case LONG_ITEM:
return 1;
case BIGINTEGER_ITEM:
return value.compareTo(((BigIntegerItem) item).value);
case STRING_ITEM:
return 1; // 1.1 > 1-sp
case LIST_ITEM:
return 1; // 1.1 > 1-1
default:
throw new AssertionError("invalid item: " + item.getClass());
}
}
@Override
public void appendTo(StringBuilder buffer) {
buffer.append(value);
}
public String toString() {
return value.toString();
}
@@ -123,9 +198,8 @@ public class VersionNumber implements Comparable<VersionNumber> {
/**
* Represents a string in the version item list, usually a qualifier.
*/
private static class StringItem
implements Item {
private String value;
private static final class StringItem implements Item {
private final String value;
StringItem(String value) {
this.value = value;
@@ -145,7 +219,8 @@ public class VersionNumber implements Comparable<VersionNumber> {
return 1;
}
switch (item.getType()) {
case INTEGER_ITEM:
case LONG_ITEM:
case BIGINTEGER_ITEM:
return -1; // 1.any < 1.1 ?
case STRING_ITEM:
@@ -155,10 +230,15 @@ public class VersionNumber implements Comparable<VersionNumber> {
return -1; // 1.any < 1-1
default:
throw new RuntimeException("invalid item: " + item.getClass());
throw new AssertionError("invalid item: " + item.getClass());
}
}
@Override
public void appendTo(StringBuilder buffer) {
buffer.append(value);
}
public String toString() {
return value;
}
@@ -168,14 +248,14 @@ public class VersionNumber implements Comparable<VersionNumber> {
* Represents a version list item. This class is used both for the global item list and for sub-lists (which start
* with '-(number)' in the version specification).
*/
private static class ListItem
extends ArrayList<Item>
implements Item {
Character separator;
private static final class ListItem extends ArrayList<Item> implements Item {
private final Character separator;
public ListItem() {}
ListItem() {
this.separator = null;
}
public ListItem(char separator) {
ListItem(char separator) {
this.separator = separator;
}
@@ -184,7 +264,7 @@ public class VersionNumber implements Comparable<VersionNumber> {
}
public boolean isNull() {
return (size() == 0);
return size() == 0;
}
void normalize() {
@@ -209,7 +289,8 @@ public class VersionNumber implements Comparable<VersionNumber> {
return first.compareTo(null);
}
switch (item.getType()) {
case INTEGER_ITEM:
case LONG_ITEM:
case BIGINTEGER_ITEM:
return -1; // 1-1 < 1.0.x
case STRING_ITEM:
@@ -234,36 +315,46 @@ public class VersionNumber implements Comparable<VersionNumber> {
return 0;
default:
throw new RuntimeException("invalid item: " + item.getClass());
throw new AssertionError("invalid item: " + item.getClass());
}
}
@Override
public void appendTo(StringBuilder buffer) {
if (separator != null) {
buffer.append((char) separator);
}
final int initLength = buffer.length();
for (Item item : this) {
if (buffer.length() > initLength) {
if (!(item instanceof ListItem))
buffer.append('.');
}
item.appendTo(buffer);
}
}
public String toString() {
StringBuilder buffer = new StringBuilder();
for (Item item : this) {
if (buffer.length() > 0) {
if (!(item instanceof ListItem))
buffer.append('.');
}
buffer.append(item);
}
if (separator != null)
return separator + buffer.toString();
else
return buffer.toString();
appendTo(buffer);
return buffer.toString();
}
}
public VersionNumber(String version) {
parseVersion(version);
}
private static final int MAX_LONGITEM_LENGTH = 18;
private void parseVersion(String version) {
private final String value;
public final ListItem items;
private final String canonical;
private VersionNumber(String version) {
this.value = version;
ListItem list = items = new ListItem();
ListItem list = this.items = new ListItem();
Stack<Item> stack = new Stack<>();
Deque<Item> stack = new ArrayDeque<>();
stack.push(list);
boolean isDigit = false;
@@ -275,14 +366,14 @@ public class VersionNumber implements Comparable<VersionNumber> {
if (c == '.') {
if (i == startIndex) {
list.add(IntegerItem.ZERO);
list.add(LongItem.ZERO);
} else {
list.add(parseItem(version.substring(startIndex, i)));
}
startIndex = i + 1;
} else if ("!\"#$%&'()*+,-/:;<=>?@[\\]^_`{|}~".indexOf(c) != -1) {
if (i == startIndex) {
list.add(IntegerItem.ZERO);
list.add(LongItem.ZERO);
} else {
list.add(parseItem(version.substring(startIndex, i)));
}
@@ -290,7 +381,7 @@ public class VersionNumber implements Comparable<VersionNumber> {
list.add(list = new ListItem(c));
stack.push(list);
} else if (Character.isDigit(c)) {
} else if (c >= '0' && c <= '9') {
if (!isDigit && i > startIndex) {
list.add(parseItem(version.substring(startIndex, i)));
startIndex = i;
@@ -322,11 +413,42 @@ public class VersionNumber implements Comparable<VersionNumber> {
list.normalize();
}
canonical = items.toString();
this.canonical = items.toString();
}
// For simple version
private VersionNumber(String version, ListItem items) {
this.value = version;
this.items = items;
this.canonical = version;
}
private static Item parseItem(String buf) {
return buf.chars().allMatch(Character::isDigit) ? new IntegerItem(buf) : new StringItem(buf);
int numberLength = 0;
boolean leadingZero = true;
for (int i = 0; i < buf.length(); i++) {
char ch = buf.charAt(i);
if (ch >= '0' && ch <= '9') {
if (ch != '0') {
leadingZero = false;
}
if (!leadingZero) {
numberLength++;
}
} else {
return new StringItem(buf);
}
}
if (numberLength == 0) {
return LongItem.ZERO;
} else if (numberLength <= MAX_LONGITEM_LENGTH) {
// Numbers which are larger than 10^19 cannot be stored as long
return new LongItem(Long.parseLong(buf));
} else {
return new BigIntegerItem(buf);
}
}
public int compareTo(String o) {
@@ -364,6 +486,4 @@ public class VersionNumber implements Comparable<VersionNumber> {
public int hashCode() {
return canonical.hashCode();
}
public static final Comparator<String> VERSION_COMPARATOR = Comparator.comparing(VersionNumber::asVersion);
}