SemVerImpl.java
package space.sunqian.fs.utils.version;
import space.sunqian.annotation.Nonnull;
import space.sunqian.annotation.Nullable;
import space.sunqian.fs.collect.ListKit;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
final class SemVerImpl implements SemVer {
private static final @Nonnull Pattern SEM_VER_PATTERN = Pattern.compile(
"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
);
private final int major;
private final int minor;
private final int patch;
private final @Nullable PreRelease preRelease;
private final @Nullable BuildMeta buildMeta;
SemVerImpl(@Nonnull String version) throws IllegalArgumentException {
Matcher matcher = SEM_VER_PATTERN.matcher(version);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid semantic version: " + version);
}
major = Integer.parseInt(matcher.group(1));
minor = Integer.parseInt(matcher.group(2));
patch = Integer.parseInt(matcher.group(3));
String g4 = matcher.group(4);
if (g4 != null) {
String[] ids = g4.split("\\.");
Object[] preReleaseIds = new Object[ids.length];
for (int i = 0; i < ids.length; i++) {
try {
Object intObj = Integer.valueOf(ids[i]);
preReleaseIds[i] = intObj;
} catch (NumberFormatException e) {
preReleaseIds[i] = ids[i];
}
}
preRelease = new PreReleaseImpl(preReleaseIds);
} else {
preRelease = null;
}
String g5 = matcher.group(5);
if (g5 != null) {
String[] ids = g5.split("\\.");
buildMeta = new BuildMetaImpl(ids);
} else {
buildMeta = null;
}
}
@Override
public int major() {
return major;
}
@Override
public int minor() {
return minor;
}
@Override
public int patch() {
return patch;
}
@Override
public @Nullable PreRelease preRelease() {
return preRelease;
}
@Override
public @Nullable BuildMeta buildMeta() {
return buildMeta;
}
@Override
public @Nonnull String toString() {
return major() + "." + minor() + "." + patch()
+ (preRelease() == null ? "" : "-" + preRelease())
+ (buildMeta() == null ? "" : "+" + buildMeta());
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch, preRelease, buildMeta);
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SemVer)) {
return false;
}
return compareTo((SemVer) obj) == 0;
}
@Override
public int compareTo(@Nonnull SemVer o) {
// Precedence MUST be calculated by separating the version into major, minor, patch and pre-release identifiers
// in that order (Build metadata does not figure into precedence).
//
// Precedence is determined by the first difference when comparing each of these identifiers from left to right
// as follows: Major, minor, and patch versions are always compared numerically.
// Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1.
if (major < o.major()) {
return -1;
}
if (major > o.major()) {
return 1;
}
if (minor < o.minor()) {
return -1;
}
if (minor > o.minor()) {
return 1;
}
if (patch < o.patch()) {
return -1;
}
if (patch > o.patch()) {
return 1;
}
// When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version:
//
// Example: 1.0.0-alpha < 1.0.0.
PreRelease oRelease = o.preRelease();
if (preRelease == null && oRelease != null) {
return 1;
}
if (preRelease != null && oRelease == null) {
return -1;
}
if (preRelease != null) {
return preRelease.compareTo(oRelease);
}
return 0;
}
private static final class PreReleaseImpl implements PreRelease {
private final @Nonnull Object @Nonnull [] identifiers;
private final @Nonnull List<@Nonnull Object> identifierList;
private PreReleaseImpl(@Nonnull Object @Nonnull [] identifiers) {
this.identifiers = identifiers;
this.identifierList = ListKit.list(identifiers);
}
@Override
public @Nonnull List<@Nonnull Object> identifiers() {
return identifierList;
}
@Override
public @Nonnull String toString() {
return identifierList.stream().map(Object::toString).collect(Collectors.joining("."));
}
@Override
public int hashCode() {
return Arrays.hashCode(identifiers);
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof PreRelease)) {
return false;
}
return compareTo((PreRelease) obj) == 0;
}
@Override
public int compareTo(@Nonnull PreRelease o) {
int size = Math.min(identifiers.length, o.identifiers().size());
for (int i = 0; i < size; i++) {
Object id = identifiers[i];
Object oId = o.identifiers().get(i);
if (id instanceof Integer) {
if (oId instanceof Integer) {
// Identifiers consisting of only digits are compared numerically.
int cmp = ((Integer) id).compareTo((Integer) oId);
if (cmp != 0) {
return cmp;
}
} else {
// Numeric identifiers always have lower precedence than non-numeric identifiers.
return -1;
}
} else {
if (oId instanceof Integer) {
// Identifiers consisting of only digits are compared numerically.
return 1;
} else {
// Identifiers with letters or hyphens are compared lexically in ASCII sort order.
int cmp = ((String) id).compareTo((String) oId);
if (cmp != 0) {
return cmp;
}
}
}
}
// A larger set of pre-release fields has a higher precedence than a smaller set,
// if all of the preceding identifiers are equal.
return Integer.compare(identifiers.length, o.identifiers().size());
}
}
private static final class BuildMetaImpl implements BuildMeta {
private final @Nonnull String @Nonnull [] identifiers;
private final @Nonnull List<@Nonnull String> identifierList;
BuildMetaImpl(@Nonnull String @Nonnull [] identifiers) {
this.identifiers = identifiers;
this.identifierList = ListKit.list(identifiers);
}
@Override
public @Nonnull List<@Nonnull String> identifiers() {
return identifierList;
}
@Override
public @Nonnull String toString() {
return String.join(".", identifiers);
}
@Override
public int hashCode() {
return Arrays.hashCode(identifiers);
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof BuildMeta)) {
return false;
}
return identifierList.equals(((BuildMeta) obj).identifiers());
}
}
}