Alternative Version implementation
Bryan Atsatt
bryan.atsatt at oracle.com
Wed Mar 23 23:36:00 UTC 2016
FWIW: I wrote the attached Version type some time back for a previous
module system effort. It assumes module system specific parsing, into a
canonical internal form, and supports string conversions using a
canonical format. Ordering is well defined.
// Bryan
On 3/23/16 2:04 PM, David M. Lloyd wrote:
> I guess you mean more like this:
>
> 1
> 1.0
> 1.00
> 1.000
> 1.1
> 1.01
> 1.001
> 1.10
> 1.010
> 1.11
> 1.011
> 1.100
> 1.101
> 1.110
> 1.111
>
> where the numeric value is most significant and otherwise the sort is
> by length ascending?
>
> On 03/23/2016 03:57 PM, Paul Benedict wrote:
>> David, I accidentally missed something first time around. I am happy
>> Neil pointed it out (thank you). This is actually the order I was
>> expecting. I am treating numbers as numbers, with more precision being
>> greater in the collation.
>>
>> 1
>> 1.0
>> 1.00
>> 1.000
>> 1.01
>> 1.001
>> 1.010
>> 1.011
>> 1.100
>> 1.101
>> 1.110
>> 1.111
>> 1.1
>> 1.10
>> 1.11
>>
>>
>> Cheers,
>> Paul
>>
>> On Wed, Mar 23, 2016 at 3:44 PM, Neil Bartlett (Paremus)
>> <neil.bartlett at paremus.com <mailto:neil.bartlett at paremus.com>> wrote:
>>
>> Thanks David, I should have awaited your reply before responding to
>> Paul. See that email for more specifics on OSGi versions.
>>
>> I wasn’t implying anything about agreement with Remi Forax or
>> anybody else. In this case, my question was really just a question.
>> However I am skeptical about the practical feasibility of creating a
>> version scheme that can bring together all the existing practices.
>>
>> I am also very concerned with your the suggested collation order in
>> which 1.1 sorts before 1.00. This is subjective of course, but I
>> find it highly counter-intuitive. If the segment looks like a number
>> then it should act like a number, unless all segments are explicitly
>> defined as strings with alphanumeric sorting.
>>
>>
>> Neil
>>
>>
>> > On 23 Mar 2016, at 20:23, David M. Lloyd
>> <david.lloyd at redhat.com <mailto:david.lloyd at redhat.com>> wrote:
>> >
>> > The OSGi specification allows (from what I can tell) arbitrary
>> strings for the last segment and that is definitely incompatible with
>> the notion of these more general version rules... this is the only
>> difference I can find though, and many practical examples of OSGi
>> versions should continue to work, which at least yields the
>> possibility of moving forward. Are there additional scenarios you
>> can identify?
>> >
>> > Maven on the other hand does not really have a specification,
>> so I just referred to of existing examples and they seem to function
>> as expected.
>> >
>> > The work I'm doing is intended as something of a bridge between
>> what is in place now (a structure which is designed for use in the
>> JDK and which implies a strict syntactical and semantic format which
>> is incompatible with a very large number of existing schemes and
>> version numbers) and a way to allow each layer to impose its own
>> policy. But I think what you are implying is that you share my
>> interpretation of Rémi Forax's opinion that a plain string is a
>> better version identifier, with no sorting/comparison or validation
>> logic, putting 100% of the responsibility for interpretation and
>> validation of the version string to the layer which defines the
>> module. Is my understanding of your position correct?
>> >
>> > On 03/23/2016 03:01 PM, Neil Bartlett (Paremus) wrote:
>> >> Hi Paul and David,
>> >>
>> >> You may consider this collation order intuitive, but it’s
>> clearly incompatible with existing version systems; in particular I’m
>> thinking of those used in OSGi and Maven.
>> >>
>> >> I really don’t know to what extent this matters, as it was my
>> understanding that JSR 376 would not define versioning of modules and
>> that this are would be left to the discretion of external tools such
>> as build systems. David can you explain the work you are doing in
>> this context?
>> >>
>> >> Regards,
>> >> Neil
>> >>
>> >>
>> >>> On 23 Mar 2016, at 18:53, Paul Benedict <pbenedict at apache.org
>> <mailto:pbenedict at apache.org>> wrote:
>> >>>
>> >>> For any of the EG members observing this list,
>> >>>
>> >>> I find David's collating order acceptable and expected. I am
>> not privy to
>> >>> Reiner's particular discussion, but it is my opinion that 1.0
>> should
>> >>> precede 1.0.0. Although both are numerically equal, one is
>> more precise --
>> >>> ambiguity should be first, precision last. I don't find this
>> to be any
>> >>> different than the alphanumerical nature of a phone book
>> where A would
>> >>> precede AA. That's not a perfect analogy but it gets my point
>> across.
>> >>>
>> >>> Cheers,
>> >>> Paul
>> >>>
>> >>> On Wed, Mar 23, 2016 at 1:46 PM, David M. Lloyd
>> <david.lloyd at redhat.com <mailto:david.lloyd at redhat.com>>
>> >>> wrote:
>> >>>
>> >>>> On 03/23/2016 09:20 AM, David M. Lloyd wrote:
>> >>>>
>> >>>>> I've gone ahead and written a new Version implementation
>> that implements
>> >>>>> the rules I've described. It seems to work OK though I am
>> having a hard
>> >>>>> time running all tests locally due to some environmental
>> problem that
>> >>>>> I'm still working on, so I don't have a webrev yet. But I
>> do have a
>> >>>>> diff that can be examined (and commented upon) at [1].
>> >>>>>
>> >>>>
>> >>>> One oddity that springs up relating to numeric versions when
>> not
>> >>>> normalizing the version string in any way is that version
>> segments leading
>> >>>> zeros parse and sort strangely. After fiddling around with
>> various
>> >>>> approaches, currently I've settled on this order:
>> >>>>
>> >>>> 1
>> >>>> 1.0
>> >>>> 1.1
>> >>>> 1.00
>> >>>> 1.01
>> >>>> 1.10
>> >>>> 1.11
>> >>>> 1.000
>> >>>> 1.001
>> >>>> 1.010
>> >>>> 1.011
>> >>>> 1.100
>> >>>> 1.101
>> >>>> 1.110
>> >>>> 1.111
>> >>>>
>> >>>> Wherein versions are sorted for length first, then for
>> value. However
>> >>>> that might be counter-intuitive if your expectation is that
>> (for example)
>> >>>> 1.0 is equal to 1.00 or at least sorts immediately before or
>> after it. A
>> >>>> good case could be made that versions should be normalized
>> to strip leading
>> >>>> zeros, and I believe the previous implementation did this
>> (either
>> >>>> intentionally or unintentionally) as an implementation
>> side-effect. The
>> >>>> downside of normalization is the extra work and extra String
>> being produced
>> >>>> as a result.
>> >>>>
>> >>>> A third option would be to reject version segments with
>> leading zeros,
>> >>>> which prevents the problem from coming up and also avoids
>> the extra copy
>> >>>> work, making the "number" production look like:
>> >>>>
>> >>>> number = ? Unicode decimal digit with values 1-9 ? { ?
>> Unicode decimal
>> >>>> digit ? }
>> >>>>
>> >>>> Any thoughts on this would be appreciated.
>> >>>> --
>> >>>> - DML
>> >>>>
>> >>
>> >
>> > --
>> > - DML
>>
>>
>
-------------- next part --------------
/*
* Copyright 2009 Oracle Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package java.lang.module;
import java.util.regex.Pattern;
/**
* A canonical, syntax agnostic version number type that supports ordered comparison.
* <p/>
* Instances may contain any number of positive numeric components and an optional
* 'preview' or 'update' string qualifier. An unqualified instance is considered a
* 'release' version. Qualifier strings may contain any of the posix visible characters
* (i.e. '\u0021' through '\u006E').
* <p/>
* During comparison:
* <ul>
* <li>Numeric components of differing lengths are normalized by zero padding.</li>
* <li>Types are compared only if numeric components are equal: PREVIEW < RELEASE < UPDATE.</li>
* <li>Qualifiers are compared,
* <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/lang/String.html#compareTo(java.lang.String)">
* lexicographically</a>, only if types and all numeric components are equal.</li>
* </ul>
* Lexicographic qualifier comparison can result in unexpected ordering (e.g. "100" < "20");
* therefore, version schemes should be mapped to use additional numeric components where
* possible.
*
* @author Bryan Atsatt
* @since Dec 10, 2008
*/
public class Version implements Matcher<Version>, Comparable<Version> {
private static final Pattern DOT_SEP = Pattern.compile("\\.");
public enum Type {
/**
* A PREVIEW version may have a qualifier and is less than a RELEASE
* or UPDATE version with equal components.
*/
PREVIEW,
/**
* A RELEASE version has no qualifier and is greater than a PREVIEW
* version with equal components and less than an UPDATE version with
* equal components.
*/
RELEASE,
/**
* An UPDATE version may have a qualifier and is greater than a RELEASE
* version with equal components.
*/
UPDATE,
}
/**
* An instance of the minimum value.
*/
public static final Version MIN_VALUE = new Version(true) {
public int compareTo(Version other) {
return other == this ? 0 : -1;
}
public String toString() {
return "min";
}
};
/**
* An instance of the maximum value.
*/
public static final Version MAX_VALUE = new Version(false) {
public int compareTo(Version other) {
return other == this ? 0 : 1;
}
public String toString() {
return "max";
}
};
/**
* An instance of the release version zero.
*/
public static Version ZERO = newReleaseVersion(0);
private static final Pattern visibleCharacters = Pattern.compile("([\\p{Graph}])+");
private Type type;
private int[] components;
private String qualifier;
/**
* Create a new PREVIEW version.
*
* @param components The components.
* @param qualifier The qualifier.
* @return The new instance.
*/
public static Version newPreviewVersion(int[] components, String qualifier) {
return new Version(Type.PREVIEW, components, qualifier);
}
/**
* Create a new RELEASE version.
*
* @param components The components.
* @return The new instance.
*/
public static Version newReleaseVersion(int... components) {
return new Version(Type.RELEASE, components, null);
}
/**
* Create a new UPDATE version.
*
* @param components The components.
* @param qualifier The qualifier.
* @return The new instance.
*/
public static Version newUpdateVersion(int[] components, String qualifier) {
return new Version(Type.UPDATE, components, qualifier);
}
/**
* Create a new version.
*
* @param type The type.
* @param components The components.
* @param qualifier The qualifier. Must be null if type == RELEASE, otherwise
* must be non-zero length.
* @return The new instance.
*/
public static Version newVersion(Type type, int[] components, String qualifier) {
return new Version(type, components, qualifier);
}
/**
* Returns the type of this instance.
*
* @return The type.
*/
public Type getType() {
return type;
}
/**
* Returns the number of components in this instance.
*
* @return The count.
*/
public int getComponentCount() {
return components.length;
}
/**
* Return the component at the specified index.
*
* @param index The index.
* @return The component.
* @throws IndexOutOfBoundsException if index is out of bounds.
*/
public int getComponentAt(int index) {
return components[index];
}
/**
* Returns the qualifier, if any.
*
* @return The qualifier if type is PREVIEW or UPDATE, null if RELEASE;
*/
public String getQualifier() {
return qualifier;
}
/**
* Compare two {@code Version} objects.
*
* @param version the {@code Version} to be compared.
* @return the value 0 if the this {@code Version} is equal to the
* {@code Version} argument; a value less than 0 if this
* {@code Version} is less than the {@code Version} argument; and a
* value greater than 0 if this {@code Version} is greater than the
* {@code Version} argument.
*/
public int compareTo(Version version) {
int result = 0;
if (this != version) {
if (version == MAX_VALUE) {
result = -1;
} else if (version == MIN_VALUE) {
result = 1;
} else {
// Are the components equal?
result = compare(components, version.components);
if (result == 0) {
// Yes. Are the types equal?
result = type.ordinal() - version.type.ordinal();
if (result == 0 && type != Type.RELEASE) {
// Yes, and there is a qualifier, so the result depends
// solely on comparing them...
result = qualifier.compareTo(version.qualifier);
}
}
}
}
return result;
}
/**
* Compare two {@code Version} objects for equality. The result is
* {@code true} if and only if the argument is not {@code null} and is a
* {@code Version} object for which compareTo() returns 0.
*
* @param obj the object to compare with.
* @return whether or not two {@code Version} objects are equal.
*/
@Override
public boolean equals(Object obj) {
return this == obj || (obj instanceof Version && (compareTo((Version) obj) == 0));
}
/**
* Returns a hash code for this {@code Version}.
*
* @return a hash code value for this {@code Version}.
*/
@Override
public int hashCode() {
int result = 17 * (type.ordinal() + 1);
for (int n : components) {
result = 37 * result + n;
}
result = 37 * result + (qualifier == null ? 0 : qualifier.hashCode());
return result;
}
/**
* Returns a {@code String} object representing this {@code Version}'s
* value.
*
* @return a string representation of the value of this {@code Version}.
*/
@Override
public String toString() {
return toString(" preview '", " update '", "'");
}
/**
* Returns a {@code String} object representing this {@code Version}'s
* value, in canonical form:
* <pre>
* digit[.digit]... [ -<previewQualifier> | _<updateQualifier> ]
* </pre>
*
* @return a string representation of the value of this {@code Version}, in
* canonical form.
*/
public String toCanonicalForm() {
if (this == MIN_VALUE) return "min";
if (this == MAX_VALUE) return "max";
return toString("-", "_", null);
}
/**
* Returns a {@code Version} object from the specified canonical
* version string.
*
* @param version The canonical version string.
* @return The {@code Version}.
* @see #toCanonicalForm()
*/
public static Version fromCanonicalForm(String version) {
if (version.equals("min")) return MIN_VALUE;
if (version.equals("max")) return MAX_VALUE;
int pi = version.indexOf("-");
int ui = version.indexOf("_");
if (pi > 0 && ui > 0) {
if (pi < ui) {
ui = -1;
} else {
pi = -1;
}
}
if (pi > 0) {
int[] components = toComponents(version.substring(0, pi));
return newPreviewVersion(components, version.substring(pi + 1));
} else if (ui > 0) {
int[] components = toComponents(version.substring(0, ui));
return newUpdateVersion(components, version.substring(ui + 1));
}
return newReleaseVersion(toComponents(version));
}
/**
* Tests if the specified version is equal to this instance.
*
* @param version The target version.
* @return {@code true} if the specified version is equal to this instance;
* {@code false} otherwise.
*/
public boolean matches(Version version) {
return equals(version);
}
private String toString(String preview, String update, String trailing) {
StringBuilder buf = new StringBuilder();
int length = components.length;
for (int i = 0; i < length; i++) {
if (i > 0) {
buf.append('.');
}
buf.append(components[i]);
}
if (length == 1) {
buf.append(".0");
}
if (type != Type.RELEASE) {
if (type == Type.PREVIEW) {
buf.append(preview);
} else {
buf.append(update);
}
buf.append(qualifier);
if (trailing != null) {
buf.append(trailing);
}
}
return buf.toString();
}
private Version(Type type, int[] components, String qualifier) {
assert (type != null);
this.type = type;
this.components = components;
this.qualifier = qualifier;
validateComponents();
validateQualifier();
}
private Version(boolean minValue) { // Special MIN/MAX value ctor.
if (minValue) {
type = Type.PREVIEW;
components = new int[]{0};
qualifier = "!";
} else {
type = Type.UPDATE;
components = new int[]{Integer.MAX_VALUE};
qualifier = "~";
}
}
private static int compare(int[] right, int[] left) {
int rLen = right.length;
int lLen = left.length;
int len = Math.max(rLen, lLen);
for (int i = 0; i < len; i++) {
int rValue = i < rLen ? right[i] : 0;
int lValue = i < lLen ? left[i] : 0;
int result = rValue - lValue;
if (result != 0) {
return result;
}
}
return 0;
}
private void validateComponents() {
if (components == null) {
throw new IllegalArgumentException("Version components must not be null.");
}
if (components.length == 0) {
throw new IllegalArgumentException("Version components must not be empty.");
}
for (int c : components) {
if (c < 0) {
throw new IllegalArgumentException("Version components must not be negative: " + this);
}
}
}
private void validateQualifier() {
if (type == Type.RELEASE) {
if (qualifier != null) {
throw new IllegalArgumentException("Version qualifier must be null.");
}
} else {
if (qualifier == null) {
throw new IllegalArgumentException("Version qualifier must not be null.");
}
if (qualifier.length() == 0) {
throw new IllegalArgumentException("Version qualifier must not be empty: " + this);
}
if (!visibleCharacters.matcher(qualifier).matches()) {
throw new IllegalArgumentException(
"Version qualifier must contain only posix visible characters: " + this);
}
}
}
private static int[] toComponents(String componentStr) {
String[] componentStrings = DOT_SEP.split(componentStr);
int[] components = new int[componentStrings.length];
for (int i = 0; i < components.length; i++) {
components[i] = Integer.parseInt(componentStrings[i].trim());
}
return components;
}
}
More information about the jpms-spec-observers
mailing list