优化 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);
}

View File

@@ -19,105 +19,135 @@ package org.jackhuang.hmcl.util.versioning;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.*;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.util.versioning.VersionNumber.isIntVersionNumber;
import static org.jackhuang.hmcl.util.versioning.VersionNumber.normalize;
import static org.junit.jupiter.api.Assertions.*;
public class VersionNumberTest {
@Test
public void testCanonical() {
VersionNumber u, v;
assertEquals("3.2", normalize("3.2.0.0"));
assertEquals("3.2-5", normalize("3.2.0.0-5"));
assertEquals("3.2", normalize("3.2.0.0-0"));
assertEquals("3.2", normalize("3.2--------"));
assertEquals("3.2", normalize("3.0002"));
assertEquals("1.7.2$%%^@&snapshot-3.1.1", normalize("1.7.2$%%^@&snapshot-3.1.1"));
assertEquals("1.99999999999999999999", normalize("1.99999999999999999999"));
assertEquals("1.99999999999999999999", normalize("1.0099999999999999999999"));
assertEquals("1.99999999999999999999", normalize("1.99999999999999999999.0"));
assertEquals("1.99999999999999999999", normalize("1.99999999999999999999--------"));
}
v = VersionNumber.asVersion("3.2.0.0");
assertEquals("3.2", v.getCanonical());
@Test
public void testIsIntVersion() {
assertFalse(isIntVersionNumber(""));
assertFalse(isIntVersionNumber(" "));
assertFalse(isIntVersionNumber("."));
assertFalse(isIntVersionNumber("1."));
assertFalse(isIntVersionNumber(".1"));
assertFalse(isIntVersionNumber(".1."));
assertFalse(isIntVersionNumber("1..8"));
assertFalse(isIntVersionNumber("1.8."));
assertFalse(isIntVersionNumber(".1.8"));
assertFalse(isIntVersionNumber("1.7.10forge1614_FTBInfinity"));
assertFalse(isIntVersionNumber("3.2-5"));
assertFalse(isIntVersionNumber("1.9999999999"));
v = VersionNumber.asVersion("3.2.0.0-5");
assertEquals("3.2-5", v.getCanonical());
assertTrue(isIntVersionNumber("0"));
assertTrue(isIntVersionNumber("1"));
assertTrue(isIntVersionNumber("0.1"));
assertTrue(isIntVersionNumber("0.1.0"));
assertTrue(isIntVersionNumber("1.8"));
assertTrue(isIntVersionNumber("1.12.2"));
assertTrue(isIntVersionNumber("1.13.1"));
assertTrue(isIntVersionNumber("1.999999999"));
assertTrue(isIntVersionNumber("999999999.0"));
}
v = VersionNumber.asVersion("3.2.0.0-0");
assertEquals("3.2", v.getCanonical());
private static void assertLessThan(String s1, String s2) {
Supplier<String> messageSupplier = () -> String.format("%s should be less than %s", s1, s2);
v = VersionNumber.asVersion("3.2--------");
assertEquals("3.2", v.getCanonical());
VersionNumber v1 = VersionNumber.asVersion(s1);
VersionNumber v2 = VersionNumber.asVersion(s2);
v = VersionNumber.asVersion("1.7.2$%%^@&snapshot-3.1.1");
assertEquals("1.7.2$%%^@&snapshot-3.1.1", v.getCanonical());
assertTrue(v1.compareTo(v2) < 0, messageSupplier);
assertTrue(v2.compareTo(v1) > 0, messageSupplier);
}
@Test
public void testComparator() {
VersionNumber u, v;
u = VersionNumber.asVersion("1.7.10forge1614_FTBInfinity");
v = VersionNumber.asVersion("1.12.2");
assertTrue(u.compareTo(v) < 0);
u = VersionNumber.asVersion("1.8.0_51");
v = VersionNumber.asVersion("1.8.0.51");
assertTrue(u.compareTo(v) < 0);
u = VersionNumber.asVersion("1.8.0_151");
v = VersionNumber.asVersion("1.8.0_77");
assertTrue(u.compareTo(v) > 0);
u = VersionNumber.asVersion("1.6.0_22");
v = VersionNumber.asVersion("1.8.0_11");
assertTrue(u.compareTo(v) < 0);
u = VersionNumber.asVersion("1.7.0_22");
v = VersionNumber.asVersion("1.7.99");
assertTrue(u.compareTo(v) < 0);
u = VersionNumber.asVersion("1.12.2-14.23.5.2760");
v = VersionNumber.asVersion("1.12.2-14.23.4.2739");
assertTrue(u.compareTo(v) > 0);
assertLessThan("1.7.10forge1614_FTBInfinity", "1.12.2");
assertLessThan("1.8.0_51", "1.8.0.51");
assertLessThan("1.8.0_77", "1.8.0_151");
assertLessThan("1.6.0_22", "1.8.0_11");
assertLessThan("1.7.0_22", "1.7.99");
assertLessThan("1.12.2-14.23.4.2739", "1.12.2-14.23.5.2760");
assertLessThan("1.9", "1.99999999999999999999");
assertLessThan("1.99999999999999999999", "1.199999999999999999999");
assertLessThan("1.99999999999999999999", "2");
assertLessThan("1.99999999999999999999", "2.0");
}
@Test
public void testSorting() {
List<String> input = Arrays.asList(
"1.10",
"1.10.2",
"1.10.2-All the Mods",
"1.10.2-AOE",
"1.10.2-AOE-1.1.5",
"1.10.2-forge2511-Age_of_Progression",
"1.10.2-forge2511-AOE-1.1.2",
"1.10.2-forge2511-ATM-E",
"1.10.2-forge2511-simple_life_2",
"1.10.2-forge2511_bxztest",
"1.10.2-forge2511_Farming_Valley",
"1.10.2-forge2511中文",
"1.10.2-FTB_Beyond",
"1.10.2-LiteLoader1.10.2",
"1.12.2",
"1.12.2_Modern_Skyblock-3.4.2",
"1.13.1",
final Comparator<String> comparator = VersionNumber.VERSION_COMPARATOR.thenComparing(String::compareTo);
final List<String> input = Collections.unmodifiableList(Arrays.asList(
"0",
"0.10.0",
"1.6.4",
"1.6.4-Forge9.11.1.1345",
"1.7.10",
"1.7.10-1614",
"1.7.10-1614-test",
"1.7.10Agrarian_Skies_2",
"1.7.10-F1614-L",
"1.7.10-FL1614_04",
"1.7.10-Forge10.13.4.1614-1.7.10",
"1.7.10-Forge1614",
"1.7.10-Forge1614.1",
"1.7.10Agrarian_Skies_2",
"1.7.10forge1614test",
"1.7.10forge1614_ATlauncher",
"1.7.10forge1614_FTBInfinity",
"1.7.10Forge1614_FTBInfinity-2.6.0",
"1.7.10Forge1614_FTBInfinity-3.0.1",
"1.7.10-Forge1614.1",
"1.7.10forge1614_ATlauncher",
"1.7.10forge1614_FTBInfinity",
"1.7.10forge1614_FTBInfinity_server",
"1.7.10forge1614test",
"1.7.10-1614",
"1.7.10-1614-test",
"1.8",
"1.8-forge1577",
"1.8.9",
"1.8.9-forge1902",
"1.9");
input.sort(Comparator.comparing(VersionNumber::asVersion));
"1.9",
"1.10",
"1.10.2",
"1.10.2-AOE",
"1.10.2-AOE-1.1.5",
"1.10.2-All the Mods",
"1.10.2-FTB_Beyond",
"1.10.2-LiteLoader1.10.2",
"1.10.2-forge2511-AOE-1.1.2",
"1.10.2-forge2511-ATM-E",
"1.10.2-forge2511-Age_of_Progression",
"1.10.2-forge2511_Farming_Valley",
"1.10.2-forge2511_bxztest",
"1.10.2-forge2511-simple_life_2",
"1.10.2-forge2511中文",
"1.12.2",
"1.12.2_Modern_Skyblock-3.4.2",
"1.13.1",
"1.99999999999999999999",
"2",
"2.0",
"2.1"));
List<String> output = new ArrayList<>(input);
output.sort(comparator);
assertIterableEquals(input, output);
Collections.shuffle(output, new Random(0));
output.sort(comparator);
assertIterableEquals(input, output);
}
}