All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kiwiproject.base.Versions Maven / Gradle / Ivy

Go to download

Kiwi is a utility library. We really like Google's Guava, and also use Apache Commons. But if they don't have something we need, and we think it is useful, this is where we put it.

There is a newer version: 4.5.2
Show newest version
package org.kiwiproject.base;

import static org.apache.commons.lang3.StringUtils.isNumeric;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotBlank;

import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;

/**
 * A few simple version comparison utilities.
 */
@UtilityClass
public class Versions {

    /**
     * Given two versions, return the higher version
     *
     * @param left  the first version to compare
     * @param right the second version to compare
     * @return the higher of the given versions
     */
    public static String higherVersion(String left, String right) {
        return versionCompare(left, right) >= 0 ? left : right;
    }

    /**
     * Returns true if the "left" version is strictly higher than the "right" version.
     *
     * @param left  the first version to compare
     * @param right the second version to compare
     * @return true if {@code left} is greater than {@code right}
     */
    public static boolean isStrictlyHigherVersion(String left, String right) {
        return versionCompare(left, right) > 0;
    }

    /**
     * Returns true if the "left" version is higher than or equal to the "right" version.
     *
     * @param left  the first version to compare
     * @param right the second version to compare
     * @return true if {@code left} is greater than or equal to {@code right}
     */
    public static boolean isHigherOrSameVersion(String left, String right) {
        return versionCompare(left, right) >= 0;
    }

    /**
     * Returns true if the "left" version is strictly lower than the "right" version.
     *
     * @param left  the first version to compare
     * @param right the second version to compare
     * @return true if {@code left} is lower than {@code right}
     */
    public static boolean isStrictlyLowerVersion(String left, String right) {
        return versionCompare(left, right) < 0;
    }

    /**
     * Returns true if the "left" version is lower than or equal to the "right" version.
     *
     * @param left  the first version to compare
     * @param right the second version to compare
     * @return true if {@code left} is less than or equal to {@code right}
     */
    public static boolean isLowerOrSameVersion(String left, String right) {
        return versionCompare(left, right) <= 0;
    }

    /**
     * Returns true if the "left" version exactly equals the "right" version.
     *
     * @param left  the first version to compare
     * @param right the second version to compare
     * @return true if {@code left} equals {@code right}
     */
    public static boolean isSameVersion(String left, String right) {
        return versionCompare(left, right) == 0;
    }

    /**
     * Performs a case-insensitive, segment by segment comparison of numeric and alphanumeric version numbers. Versions
     * are split on periods and dashes. For example, the segments of "2.5.0" are "2", "5", and "0" while the segments of
     * "1.0.0-alpha.3" are "1", "0", "0", "alpha", and "3". Returns -1, 0, or 1 as the "left" version is less than,
     * equal to, or greater than the "right" version. (These return values correspond to the values returned by the
     * {@link Integer#signum(int)} function.)
     * 

* When a segment is determined to be non-numeric, a case-insensitive string comparison is performed. When the * number of segments in the version are different, then the general logic is that the shorter segment is * the higher version. This covers commons situations such as 1.0.0-SNAPSHOT, 1.0.0-alpha, and 1.0.0-beta.2, which * should all be lower versions than 1.0.0. * * @param left the first version number (e.g. "1.2.3" or "2.0.0-alpha1") * @param right the second version number (e.g. "1.2.4" or "2.0.0-alpha2") * @return -1 if "left" is less than "right", 0 if the versions are equal, and 1 if "left" is higher than "right" * @implNote The current implementation works best when versions have the same number of segments, e.g. comparing * 2.1.0 vs 2.0.0. It also works fine with different number of segments when those different segments are numeric, * such as 1.0.0 vs 1.0.0.42 (the latter is higher). It also handles most normal cases when the last segments are * different and are non-numeric, e.g. 1.0.0 should be considered a higher version than 1.0.0-SNAPSHOT or * 1.0.0-alpha. There are various edge cases that might report results that might not be what you expect; for * example, should 2.0.0-beta.1 be a higher or lower version than 2.0.0-beta? Currently 2.0.0-beta is reported as * the higher version due to the simple implementation. However, note that 2.0.0-beta1 would be reported as higher * than 2.0.0-beta (because the String "beta" is "greater than" the String "beta" using (Java) string comparison. * @see Integer#signum(int) */ public static int versionCompare(String left, String right) { checkArgumentNotBlank(left, "left version cannot be blank"); checkArgumentNotBlank(right, "right version cannot be blank"); if (left.equals(right)) { // if there's an exact string match, exit early since they are equal return 0; } // 1. normalize versions to lowercase so all alphanumeric comparisons are case-insensitive // 2. split versions on dot/period and dash String[] leftParts = lowerCaseAndSplit(left); String[] rightParts = lowerCaseAndSplit(right); // find the first non-equal segment index (or the index of the last segment of the shorter version) int pos = indexOfFirstUnequalOrLastCommonSegment(leftParts, rightParts); // compare first non-equal value if we found an unequal segment before the end if (pos < leftParts.length && pos < rightParts.length) { return contextuallyCompare(leftParts[pos], rightParts[pos]); } // one of the given version arguments is a substring of the other. e.g. 1.0.0-alpha1 contains 1.0.0 // if all segments are numeric, then whichever is longer is the higher version, // e.g. 3.0.1 > 3.0 and 2.5.10.1 > 2.5.10 if (allAreNumericIn(leftParts) && allAreNumericIn(rightParts)) { return Integer.signum(leftParts.length - rightParts.length); } // Not all segments are numeric. These segments are assumed to contain things like alpha, beta, SNAPSHOT, etc. // Handle special cases such as alpha[.n], beta[.n], Mn (i.e. milestone, like M1, M2) in a generic manner such // that whichever part is longer is considered the *lower* version. This simple logic means that 1.0.0.alpha1 // is a lower version than 1.0.0, 2.5.0-SNAPSHOT is a lower version than 2.5.0, and so on. return Integer.signum(rightParts.length - leftParts.length); } private static int indexOfFirstUnequalOrLastCommonSegment(String[] leftParts, String[] rightParts) { var pos = 0; while (pos < leftParts.length && pos < rightParts.length && leftParts[pos].equals(rightParts[pos])) { pos++; } return pos; } private static String[] lowerCaseAndSplit(String version) { return version.toLowerCase().split("[.-]"); } private static int contextuallyCompare(String leftPart, String rightPart) { if (isNumeric(leftPart) && isNumeric(rightPart)) { return compareNumeric(leftPart, rightPart); } return compareString(leftPart, rightPart); } private static int compareNumeric(String leftPart, String rightPart) { int diff = Integer.valueOf(leftPart).compareTo(Integer.valueOf(rightPart)); return Integer.signum(diff); } private static int compareString(String leftPart, String rightPart) { // Both args should be lowercase, so can compare exactly var result = StringUtils.compare(leftPart, rightPart); return Integer.compare(result, 0); } private static boolean allAreNumericIn(String[] elements) { return Arrays.stream(elements).allMatch(StringUtils::isNumeric); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy