blob: e468fc6ddd2cb0fa95efcd4bf7bdfa966e9f882e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.sis.measure;
import java.util.Map;
import java.util.HashMap;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.text.Format;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import java.security.AccessController;
import javax.measure.Unit;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.Localized;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.internal.util.LocalizedParseException;
import org.apache.sis.internal.util.FinalFieldSetter;
import org.apache.sis.internal.util.Numerics;
/**
* Parses and formats {@link Range} instances according the given locale.
* This class complies to the format described in the <a href="http://en.wikipedia.org/wiki/ISO_31-11">ISO 31-11</a>
* standard, except that the minimal and maximal values are separated by the "{@code …}" character
* instead than coma. More specifically, the format is defined as below:
*
* <ul>
* <li>If the range {@linkplain Range#isEmpty() is empty}, then the range is represented by "{@code {}}".</li>
* <li>Otherwise if the {@linkplain Range#getMinValue() minimal value} is equals to the
* {@linkplain Range#getMaxValue() maximal value}, then that single value is formatted
* inside braces as in "{@code {value}}".</li>
* <li>Otherwise the minimal and maximal values are formatted inside bracket or parenthesis,
* depending on whether each endpoint is inclusive or exclusive:
* <ul>
* <li>"{@code [min … max]}" if both endpoints are inclusive (<cite>closed interval</cite>);</li>
* <li>"{@code (min … max)}" if both endpoints are exclusive (<cite>open interval</cite>);</li>
* <li>or a mix of both styles if an endpoint is inclusive while the other is exclusive.</li>
* </ul>
* The "{@code ∞}" symbol is used in place of {@code min} or {@code max} for unbounded ranges.</li>
* </ul>
*
* If the range to format is an instance of {@link MeasurementRange}, then the
* {@linkplain Unit unit of measurement} is appended except for empty ranges.
*
* <div class="section">Lenient parsing</div>
* At parsing time, the above formatting rules are relaxed as below:
*
* <ul>
* <li>Empty ranges can be represented by "{@code []}"or "{@code ()}" in addition to the
* standard "{@code {}}".</li>
* <li>The braces are optional for singleton values, i.e. "{@code value}" is accepted
* as well as "{@code {value}}".</li>
* </ul>
*
* <div class="section">Range type and type of range elements</div>
* The kind of ranges created by the {@link #parse(String) parse(…)} methods is determined
* by the type of range elements:
*
* <ul>
* <li>If the elements type is assignable to {@link Date}, then the {@code parse(…)} methods
* will create {@code Range<Date>} objects.</li>
* <li>If the elements type is assignable to {@link Number}, then:
* <ul>
* <li>If the text to parse contains a {@linkplain Unit unit of measurement}, then
* the {@code parse(…)} methods will create {@link MeasurementRange} objects.</li>
* <li>Otherwise the {@code parse(…)} methods will create {@link NumberRange} objects.</li>
* </ul>
* </li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
*
* @see Range#toString()
* @see <a href="http://en.wikipedia.org/wiki/ISO_31-11">Wikipedia: ISO 31-11</a>
*
* @since 0.3
* @module
*/
public class RangeFormat extends Format implements Localized {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 2459948572315667868L;
/**
* The constant value for {@link FieldPosition} which designate the minimal value.
*
* @see Field#MIN_VALUE
*/
private static final int MIN_VALUE_FIELD = 0;
/**
* The constant value for {@link FieldPosition} which designate the maximal value.
*
* @see Field#MAX_VALUE
*/
private static final int MAX_VALUE_FIELD = 1;
/**
* The constant value for {@link FieldPosition} which designate the unit of measurement.
*
* @see Field#UNIT
*/
private static final int UNIT_FIELD = 2;
/**
* Constants that are used as attribute keys in the iterator returned from
* {@link RangeFormat#formatToCharacterIterator(Object)}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.3
* @since 0.3
* @module
*/
public static final class Field extends FormatField {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 2000378602311146796L;
/**
* Creates a new field of the given name. The given name shall
* be identical to the name of the public static constant.
*/
private Field(final String name, final int fieldID) {
super(name, fieldID);
}
/**
* Identifies the minimal value field in a range.
* When formatting a string, this value may be specified to the {@link FieldPosition}
* constructor in order to get the bounding index where the minimal value has been written.
*/
public static final Field MIN_VALUE = new Field("MIN_VALUE", MIN_VALUE_FIELD);
/**
* Identifies the maximal value field in a range.
* When formatting a string, this value may be specified to the {@link FieldPosition}
* constructor in order to get the bounding index where the maximal value has been written.
*/
public static final Field MAX_VALUE = new Field("MAX_VALUE", MAX_VALUE_FIELD);
/**
* Identifies the unit field in a range, if any.
* When formatting a string, this value may be specified to the {@link FieldPosition}
* constructor in order to get the bounding index where the unit has been written.
*/
public static final Field UNIT = new Field("UNIT", UNIT_FIELD);
/**
* Returns the field constant for the given numeric identifier.
*/
static Field forCode(final int field) {
switch (field) {
case MIN_VALUE_FIELD: return MIN_VALUE;
case MAX_VALUE_FIELD: return MAX_VALUE;
case UNIT_FIELD: return UNIT;
default: throw new AssertionError(field);
}
}
}
// All "character" fields below are code point values.
/**
* The character opening an empty range or a range containing only one element.
* The default value is <code>'&#123;'</code>.
*/
private final int openSet;
/**
* The character opening a range in which the minimal value is inclusive.
* The default value is {@code '['}.
*/
private final int openInclusive;
/**
* The character opening a range in which the minimal value is exclusive.
* The default value is {@code '('}. Note that the {@code ']'} character
* is also sometime used.
*/
private final int openExclusive;
/**
* An alternative character opening a range in which the minimal value is exclusive.
* This character is not used for formatting (only {@link #openExclusive} is used),
* but is accepted during parsing. The default value is {@code ']'}.
*/
private final int openExclusiveAlt;
/**
* The character closing an empty range or a range containing only one element.
* The default value is <code>'&#125;'</code>.
*/
private final int closeSet;
/**
* The character closing a range in which the maximal value is inclusive.
* The default value is {@code ']'}.
*/
private final int closeInclusive;
/**
* The character closing a range in which the maximal value is exclusive.
* The default value is {@code ')'}. Note that the {@code '['} character
* is also sometime used.
*/
private final int closeExclusive;
/**
* An alternative character closing a range in which the maximal value is exclusive.
* This character is not used for formatting (only {@link #closeExclusive} is used),
* but is accepted during parsing. The default value is {@code '['}.
*/
private final int closeExclusiveAlt;
/**
* The string to use as a separator between minimal and maximal value, not including
* whitespaces. The default value is {@code "…"} (Unicode 2026).
*/
private final String separator;
/**
* Symbols used by this format, inferred from {@link DecimalFormatSymbols}.
*/
private final char minusSign;
/**
* Symbols used by this format, inferred from {@link DecimalFormatSymbols}.
*/
private final String infinity;
/**
* {@code true} if {@code RangeFormat} shall use the alternate form at formatting time.
* This flag as no effect on parsing, since both forms are accepted.
*
* @see #isAlternateForm()
*/
private boolean alternateForm;
/**
* The type of the range components. Valid types are {@link Number}, {@link Angle},
* {@link Date} or a subclass of those types. This value determines the kind of range
* to be created by the parse method:
*
* <ul>
* <li>{@code NumberRange<?>} if the element type is assignable to {@link Number} or {@link Angle}.</li>
* <li>{@code Range<Date>} if the element type is assignable to {@link Date}.</li>
* </ul>
*
* @see Range#getElementType()
*/
protected final Class<?> elementType;
/**
* The format to use for parsing and formatting the range components.
* The format is determined from the {@linkplain #elementType element type}:
*
* <ul>
* <li>{@link AngleFormat} if the element type is assignable to {@link Angle}.</li>
* <li>{@link NumberFormat} if the element type is assignable to {@link Number}.</li>
* <li>{@link DateFormat} if the element type is assignable to {@link Date}.</li>
* </ul>
*/
protected final Format elementFormat;
/**
* The format for unit of measurement, or {@code null} if none. This is non-null if and
* only if {@link #elementType} is assignable to {@link Number} but not to {@link Angle}.
*/
protected final UnitFormat unitFormat;
/**
* Whether we should insert a space between the bracket and the unit symbol.
*
* @see #insertSpaceBeforeUnit(Unit)
*/
private transient Map<Unit<?>,Boolean> insertSpaceBeforeUnit;
/**
* The locale for error message, or {@code null} for the default.
*/
private Locale locale;
/**
* Creates a new format for parsing and formatting {@linkplain NumberRange number ranges}
* using the {@linkplain Locale#getDefault() default locale}.
*/
public RangeFormat() {
this(Locale.getDefault(Locale.Category.FORMAT));
locale = Locale.getDefault(Locale.Category.DISPLAY);
}
/**
* Creates a new format for parsing and formatting {@linkplain NumberRange number ranges}
* using the given locale.
*
* @param locale the locale for parsing and formatting range components.
*/
public RangeFormat(final Locale locale) {
this(locale, Number.class);
}
/**
* Creates a new format for parsing and formatting {@code Range<Date>}
* using the given locale and timezone.
*
* @param locale the locale for parsing and formatting range components.
* @param timezone the timezone for the date to be formatted.
*/
public RangeFormat(final Locale locale, final TimeZone timezone) {
this(locale, Date.class);
((DateFormat) elementFormat).setTimeZone(timezone);
}
/**
* Creates a new format for parsing and formatting {@linkplain Range ranges} of
* the given element type using the given locale. The element type is typically
* {@code Date.class} or some subclass of {@code Number.class}.
*
* @param locale the locale for parsing and formatting range components.
* @param elementType the type of range components.
* @throws IllegalArgumentException if the given type is not recognized by this constructor.
*/
public RangeFormat(final Locale locale, final Class<?> elementType) throws IllegalArgumentException {
ArgumentChecks.ensureNonNull("locale", locale);
ArgumentChecks.ensureNonNull("elementType", elementType);
this.locale = locale;
this.elementType = elementType;
if (Angle.class.isAssignableFrom(elementType)) {
elementFormat = AngleFormat.getInstance(locale);
unitFormat = null;
} else if (Number.class.isAssignableFrom(elementType)) {
elementFormat = NumberFormat.getNumberInstance(locale);
unitFormat = new UnitFormat(locale);
} else if (Date.class.isAssignableFrom(elementType)) {
elementFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
unitFormat = null;
} else {
throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, elementType));
}
final DecimalFormatSymbols ds;
if (elementFormat instanceof DecimalFormat) {
ds = ((DecimalFormat) elementFormat).getDecimalFormatSymbols();
} else {
ds = DecimalFormatSymbols.getInstance(locale);
}
minusSign = ds.getMinusSign();
infinity = ds.getInfinity();
openSet = '{';
openInclusive = '['; // Future SIS version may determine those characters from the locale.
openExclusive = '('; // We may also provide an 'applyPattern(String)' method for setting those char.
openExclusiveAlt = ']';
closeSet = '}';
closeInclusive = ']';
closeExclusive = ')';
closeExclusiveAlt = '[';
separator = "…";
}
/**
* Returns {@code true} if the given character is any of the opening bracket characters.
*/
private boolean isOpen(final int c) {
return (c == openInclusive) || (c == openExclusive) || (c == openExclusiveAlt);
}
/**
* Returns {@code true} if the given character is any of the closing bracket characters.
*/
private boolean isClose(final int c) {
return (c == closeInclusive) || (c == closeExclusive) || (c == closeExclusiveAlt);
}
/**
* Returns this formatter locale. This is the locale specified at construction time if any,
* or the {@linkplain Locale#getDefault() default locale} at construction time otherwise.
*
* @return this formatter locale (never {@code null}).
*
* @since 1.0
*/
@Override
public Locale getLocale() {
return locale;
}
/**
* Returns the pattern used by {@link #elementFormat} for formatting the minimum and
* maximum values. If the element format does not use pattern, returns {@code null}.
*
* @param localized {@code true} for returning the localized pattern, or {@code false} for the unlocalized one.
* @return the pattern, or {@code null} if the {@link #elementFormat} doesn't use pattern.
*
* @see DecimalFormat#toPattern()
* @see SimpleDateFormat#toPattern()
* @see AngleFormat#toPattern()
*/
public String getElementPattern(final boolean localized) {
final Format format = elementFormat;
if (format instanceof DecimalFormat) {
final DecimalFormat df = (DecimalFormat) format;
return localized ? df.toLocalizedPattern() : df.toPattern();
}
if (format instanceof SimpleDateFormat) {
final SimpleDateFormat df = (SimpleDateFormat) format;
return localized ? df.toLocalizedPattern() : df.toPattern();
}
if (format instanceof AngleFormat) {
return ((AngleFormat) format).toPattern();
}
return null;
}
/**
* Sets the pattern to be used by {@link #elementFormat} for formatting the minimum and
* maximum values.
*
* @param pattern the new pattern.
* @param localized {@code true} if the given pattern is localized.
* @throws IllegalStateException if the {@link #elementFormat} does not use pattern.
*
* @see DecimalFormat#applyPattern(String)
* @see SimpleDateFormat#applyPattern(String)
* @see AngleFormat#applyPattern(String)
*/
public void setElementPattern(final String pattern, final boolean localized) {
final Format format = elementFormat;
if (format instanceof DecimalFormat) {
final DecimalFormat df = (DecimalFormat) format;
if (localized) {
df.applyLocalizedPattern(pattern);
} else {
df.applyPattern(pattern);
}
} else if (format instanceof SimpleDateFormat) {
final SimpleDateFormat df = (SimpleDateFormat) format;
if (localized) {
df.applyLocalizedPattern(pattern);
} else {
df.applyPattern(pattern);
}
} else if (format instanceof AngleFormat) {
((AngleFormat) format).applyPattern(pattern);
} else {
throw new IllegalStateException();
}
}
/**
* Returns {@code true} if this {@code RangeFormat} shall use the alternate form at
* formatting time. The alternate form expresses open intervals like {@code ]a…b[}
* instead of {@code (a…b)}.
*
* <p>This flag has no effect on parsing, since the parser accepts both forms.</p>
*
* @return {@code true} for using the alternate format instead of the default format.
*/
public boolean isAlternateForm() {
return alternateForm;
}
/**
* Sets whether this {@code RangeFormat} shall use the alternate form at formatting time.
* The alternate form expresses open intervals like {@code ]a…b[} instead of {@code (a…b)}.
*
* @param alternateForm {@code true} for using the alternate format, or {@code false} for using the default format.
*/
public void setAlternateForm(final boolean alternateForm) {
this.alternateForm = alternateForm;
}
/**
* Returns whether we should insert a space between the bracket and the unit symbol.
* We cache the result because checking for this condition forces us to format the unit symbol twice.
*/
private boolean insertSpaceBeforeUnit(final Unit<?> unit) {
if (insertSpaceBeforeUnit == null) {
insertSpaceBeforeUnit = new HashMap<>();
}
Boolean value = insertSpaceBeforeUnit.get(unit);
if (value == null) {
final String symbol = unitFormat.format(unit);
value = !symbol.isEmpty() && Character.isLetterOrDigit(symbol.codePointAt(0));
insertSpaceBeforeUnit.put(unit, value);
}
return value;
}
/**
* Returns the {@code *_FIELD} constant for the given field position, or -1 if none.
*/
private static int getField(final FieldPosition position) {
if (position != null) {
final Format.Field field = position.getFieldAttribute();
if (field instanceof Field) {
return ((Field) field).field;
}
return position.getField();
}
return -1;
}
/**
* Casts the given object to a {@code Range}, or throws an {@code IllegalArgumentException}
* if the given object is not a {@code Range} instance.
*/
private static Range<?> cast(final Object range) throws IllegalArgumentException {
if (range instanceof Range<?>) {
return (Range<?>) range;
}
final String message;
if (range == null) {
message = Errors.format(Errors.Keys.NullArgument_1, "range");
} else {
message = Errors.format(Errors.Keys.IllegalArgumentClass_3, "range", Range.class, range.getClass());
}
throw new IllegalArgumentException(message);
}
/**
* Formats a {@link Range} and appends the resulting text to a given string buffer.
* See the <a href="#skip-navbar_top">class javadoc</a> for a description of the format.
*
* @param range the {@link Range} object to format.
* @param toAppendTo where the text is to be appended.
* @param pos identifies a field in the formatted text, or {@code null} if none.
* @return the string buffer passed in as {@code toAppendTo}, with formatted text appended.
* @throws IllegalArgumentException if this formatter can not format the given object.
*/
@Override
public StringBuffer format(final Object range, final StringBuffer toAppendTo, final FieldPosition pos) {
format(cast(range), toAppendTo, pos, null);
return toAppendTo;
}
/**
* Implementation of the format methods.
*
* @param range the range to format.
* @param toAppendTo where the text is to be appended.
* @param pos identifies a field in the formatted text, or {@code null} if none.
* @param characterIterator the character iterator for which the attributes need to be set, or null if none.
*/
@SuppressWarnings("fallthrough")
private void format(final Range<?> range, final StringBuffer toAppendTo, final FieldPosition pos,
final FormattedCharacterIterator characterIterator)
{
/*
* Special case for an empty range. This is typically formatted as "{}". The field
* position is unconditionally set to the empty substring inside the brackets.
*/
int fieldPos = getField(pos);
if (range.isEmpty()) {
toAppendTo.appendCodePoint(openSet);
if (fieldPos >= MIN_VALUE_FIELD && fieldPos <= UNIT_FIELD) {
final int p = toAppendTo.length();
pos.setBeginIndex(p); // First index, inclusive.
pos.setEndIndex (p); // Last index, exclusive
}
toAppendTo.appendCodePoint(closeSet);
return;
}
/*
* Format a non-empty range by looping over all possible fields.
*
* Secial case: if minimal and maximal values are the same,
* formats only the maximal value.
*/
final Comparable<?> minValue = range.getMinValue();
final Comparable<?> maxValue = range.getMaxValue();
final boolean isSingleton = (minValue != null) && minValue.equals(maxValue);
int field = MIN_VALUE_FIELD;
if (isSingleton) {
if (fieldPos == MIN_VALUE_FIELD) {
fieldPos = MAX_VALUE_FIELD;
}
field = MAX_VALUE_FIELD;
}
toAppendTo.appendCodePoint( // Select the char for the first condition to be true below:
isSingleton ? openSet :
range.isMinIncluded() ? openInclusive :
alternateForm ? openExclusiveAlt :
/* otherwise */ openExclusive);
for (; field <= UNIT_FIELD; field++) {
final Object value;
switch (field) {
case MIN_VALUE_FIELD: value = minValue; break;
case MAX_VALUE_FIELD: value = maxValue; break;
case UNIT_FIELD: value = range.unit(); break;
default: throw new AssertionError(field);
}
int startPosition = toAppendTo.length();
if (value == null) {
switch (field) {
case MIN_VALUE_FIELD: toAppendTo.append(minusSign != '-' ? minusSign : '−'); // Fall through
case MAX_VALUE_FIELD: toAppendTo.append(infinity); break;
}
} else {
final Format format;
if (field == UNIT_FIELD) {
if (insertSpaceBeforeUnit((Unit) value)) {
startPosition = toAppendTo.append(' ').length();
}
format = unitFormat;
} else {
format = elementFormat;
}
Numerics.useScientificNotationIfNeeded(format, value, (f,v) -> {
if (characterIterator != null) {
characterIterator.append(f.formatToCharacterIterator(v), toAppendTo);
} else {
f.format(v, toAppendTo, new FieldPosition(-1));
}
return null;
});
}
/*
* At this point, the field has been formatted. Now store the field index,
* then append the separator between this field and the next one.
*/
if (characterIterator != null) {
characterIterator.addFieldLimit(Field.forCode(field), value, startPosition);
}
if (field == fieldPos) {
pos.setBeginIndex(startPosition);
pos.setEndIndex(toAppendTo.length());
}
switch (field) {
case MIN_VALUE_FIELD: {
toAppendTo.append(' ').append(separator).append(' ');
break;
}
case MAX_VALUE_FIELD: { // Select the char for the first condition to be true below:
toAppendTo.appendCodePoint(
isSingleton ? closeSet :
range.isMaxIncluded() ? closeInclusive :
alternateForm ? closeExclusiveAlt :
/* otherwise */ closeExclusive);
break;
}
}
}
}
/**
* Formats a range as an attributed character iterator.
* Callers can iterate and queries the attribute values as in the following example:
*
* {@preformat java
* AttributedCharacterIterator it = rangeFormat.formatToCharacterIterator(myRange);
* for (char c=it.first(); c!=AttributedCharacterIterator.DONE; c=c.next()) {
* // 'c' is a character from the formatted string.
* if (it.getAttribute(RangeFormat.Field.MIN_VALUE) != null) {
* // If we enter this block, then the character 'c' is part of the minimal value,
* // This field extends from it.getRunStart(MIN_VALUE) to it.getRunLimit(MIN_VALUE).
* }
* }
* }
*
* Alternatively, if the current {@linkplain AttributedCharacterIterator#getIndex() iterator
* index} is before the start of the minimum value field, then the starting position of that
* field can be obtained directly by {@code it.getRunLimit(MIN_VALUE)}. If the current iterator
* index is inside the minimum value field, then the above method call will rather returns the
* end of that field. The same strategy works for other all fields too.
*
* <p>The returned character iterator contains all {@link java.text.NumberFormat.Field},
* {@link java.text.DateFormat.Field} or {@link org.apache.sis.measure.AngleFormat.Field}
* attributes in addition to the {@link Field} ones. Consequently the same character may
* have more than one attribute.</p>
*
* <p>In Apache SIS implementation, the returned character iterator also implements the
* {@link CharSequence} interface for convenience.</p>
*
* @param range the {@link Range} object to format.
* @return a character iterator together with the attributes describing the formatted value.
* @throws IllegalArgumentException if {@code value} if not an instance of {@link Range}.
*/
@Override
public AttributedCharacterIterator formatToCharacterIterator(final Object range) {
final StringBuffer buffer = new StringBuffer();
final FormattedCharacterIterator it = new FormattedCharacterIterator(buffer);
format(cast(range), buffer, null, it);
return it;
}
/**
* Parses text from a string to produce a range. The default implementation delegates to
* {@link #parse(String)} with no additional work.
*
* @param source the text, part of which should be parsed.
* @return a range parsed from the string.
* @throws ParseException if the given string can not be fully parsed.
*/
@Override
public Object parseObject(final String source) throws ParseException {
return parse(source);
}
/**
* Parses text from a string to produce a range. The default implementation delegates to
* {@link #parse(String, ParsePosition)} with no additional work.
*
* @param source the text, part of which should be parsed.
* @param pos index and error index information as described above.
* @return a range parsed from the string, or {@code null} in case of error.
*/
@Override
public Object parseObject(final String source, final ParsePosition pos) {
return parse(source, pos);
}
/**
* Parses text from the given string to produce a range. This method use the full string.
* If there is some unparsed characters after the parsed range, then this method thrown an
* exception.
*
* @param source the text to parse.
* @return the parsed range (never {@code null}).
* @throws ParseException if the given string can not be fully parsed.
*/
public Range<?> parse(final String source) throws ParseException {
final ParsePosition pos = new ParsePosition(0);
UnconvertibleObjectException failure = null;
try {
final Range<?> range = tryParse(source, pos);
if (range != null) {
return range;
}
} catch (UnconvertibleObjectException e) {
failure = e;
}
throw new LocalizedParseException(locale, elementType, source, pos).initCause(failure);
}
/**
* Parses text from a string to produce a range. The method attempts to parse text starting
* at the index given by {@code pos}. If parsing succeeds, then the index of {@code pos} is
* updated to the index after the last character used, and the parsed range is returned. If
* an error occurs, then the index of {@code pos} is not changed, the error index of {@code pos}
* is set to the index of the character where the error occurred, and {@code null} is returned.
*
* @param source the text, part of which should be parsed.
* @param pos index and error index information as described above.
* @return a range parsed from the string, or {@code null} in case of error.
*/
public Range<?> parse(final String source, final ParsePosition pos) {
final int origin = pos.getIndex();
Range<?> range;
try {
// Remainder: tryParse may return null.
range = tryParse(source, pos);
} catch (UnconvertibleObjectException e) {
// Ignore - the error will be reported through the error index.
range = null;
}
if (range != null) {
pos.setErrorIndex(-1);
} else {
pos.setIndex(origin);
}
return range;
}
/**
* Tries to parse the given text. In case of success, the error index is undetermined and
* need to be reset to -1. In case of failure (including an exception being thrown), the
* parse index is undetermined and need to be reset to its initial value.
*/
@SuppressWarnings({"unchecked","rawtypes"})
private Range<?> tryParse(final String source, final ParsePosition pos)
throws UnconvertibleObjectException
{
final int length = source.length();
/*
* Skip leading whitespace and find the first non-blank character. It is usually an opening bracket,
* except if minimal and maximal values are the same in which case the brackets may be omitted.
*/
int index, c;
for (index = pos.getIndex(); ; index += Character.charCount(c)) {
if (index >= length) {
pos.setErrorIndex(length);
return null;
}
c = source.codePointAt(index);
if (!Character.isWhitespace(c)) break;
}
final Object minValue, maxValue;
final boolean isMinIncluded, isMaxIncluded;
if (!isOpen(c)) {
/*
* No bracket, or curly bracket. We have eigher an empty range (as in "{}") or a single value for the range.
* The braces are optional for single value. In other words, this block parses all of the following cases:
*
* - {}
* - {value}
* - value (not standard, but accepted by this parser)
*/
final boolean hasBraces = (c == openSet);
if (hasBraces) {
// Skip the opening brace and following whitespaces.
while ((index += Character.charCount(c)) < length) {
c = source.codePointAt(index);
if (!Character.isWhitespace(c)) break;
}
}
if (hasBraces && c == closeSet) {
// Empty range represented by {}
minValue = maxValue = valueOfNil();
isMinIncluded = isMaxIncluded = false;
} else {
// Singleton value, with or without braces.
pos.setIndex(index);
final Object value = elementFormat.parseObject(source, pos);
if (value == null) {
return null;
}
pos.setErrorIndex(index); // In case of failure during the conversion.
minValue = maxValue = convert(value);
index = pos.getIndex();
isMinIncluded = isMaxIncluded = true;
}
if (hasBraces) {
// Skip whitespaces, then skip the closing brace.
// Absence of closing brace is considered an error.
do {
if (index >= length) {
pos.setErrorIndex(length);
return null;
}
c = source.codePointAt(index);
index += Character.charCount(c);
} while (Character.isWhitespace(c));
if (c != closeSet) {
pos.setErrorIndex(index - Character.charCount(c));
return null;
}
pos.setIndex(index);
}
} else {
/*
* We found an opening bracket. Skip the whitespaces. If the next character is a closing bracket,
* then we have an empty range. The later case is an extension to the standard format since empty
* ranges are usually represented by {} instead than [].
*/
isMinIncluded = (c == openInclusive);
do { // Skip whitespaces.
index += Character.charCount(c);
if (index >= length) {
pos.setErrorIndex(length);
return null;
}
c = source.codePointAt(index);
} while (Character.isWhitespace(c));
if (isClose(c)) {
pos.setErrorIndex(index); // In case of failure during the conversion.
minValue = maxValue = valueOfNil();
isMaxIncluded = false;
index += Character.charCount(c);
} else {
/*
* At this point, we have determined that the range is non-empty and there is at least one value to parse.
* First, parse the minimal value. If we fail to parse, check if it was the infinity value. Note that "-∞"
* and "∞" should have been parsed successfully if the format is DecimalFormat, but not necessarily "−∞".
* The difference is in the character used for the minus sign (ASCII hyphen versus Unicode minus sign).
*/
pos.setIndex(index);
int savedIndex = index;
Object value = elementFormat.parseObject(source, pos);
if (value == null) {
if (c == minusSign || c == '−') {
index += Character.charCount(c);
}
if (!source.regionMatches(index, infinity, 0, infinity.length())) {
return null;
}
pos.setIndex(index += infinity.length());
}
pos.setErrorIndex(savedIndex); // In case of failure during the conversion.
minValue = convert(value);
/*
* Parsing of 'minValue' succeed and its type is valid. Now look for the separator. If it is not present,
* then assume that we have a single value for the range. The default RangeFormat implementation does not
* format brackets in such case (see the "No bracket" case above), but we make the parser tolerant to the
* case where the brackets are present.
*/
for (index = pos.getIndex(); ; index += Character.charCount(c)) {
if (index >= length) {
pos.setErrorIndex(length);
return null;
}
c = source.codePointAt(index);
if (!Character.isWhitespace(c)) break;
}
final String separator = this.separator;
if (source.regionMatches(index, separator, 0, separator.length())) {
index += separator.length();
for (;; index += Character.charCount(c)) {
if (index >= length) {
pos.setErrorIndex(length);
return null;
}
c = source.codePointAt(index);
if (!Character.isWhitespace(c)) break;
}
/*
* Now parse the maximum value. A special case is applied for infinity value
* in a similar way than we did for the minimal value.
*/
pos.setIndex(index);
value = elementFormat.parseObject(source, pos);
if (value == null) {
if (!source.regionMatches(index, infinity, 0, infinity.length())) {
return null;
}
pos.setIndex(index += infinity.length());
}
pos.setErrorIndex(index); // In case of failure during the conversion.
maxValue = convert(value);
/*
* Skip one last time the whitespaces. The check for the closing bracket (which is mandatory)
* is performed outside the "if" block since it is common to the two "if ... else" cases.
*/
for (index = pos.getIndex(); ; index += Character.charCount(c)) {
if (index >= length) {
pos.setErrorIndex(length);
return null;
}
c = source.charAt(index);
if (!Character.isWhitespace(c)) break;
}
} else {
maxValue = minValue;
}
if (!isClose(c)) {
pos.setErrorIndex(index);
return null;
}
index += Character.charCount(c);
isMaxIncluded = (c == closeInclusive);
}
pos.setIndex(index);
}
/*
* Parses the unit, if any. The units are always optional: if we can not parse
* them, then we will consider that the parsing stopped before the unit.
*/
Unit<?> unit = null;
if (unitFormat != null) {
while (index < length) {
c = source.codePointAt(index);
if (Character.isWhitespace(c)) {
index += Character.charCount(c);
continue;
}
// At this point we found a character that could be
// the beginning of a unit symbol. Try to parse that.
pos.setIndex(index);
unit = unitFormat.parse(source, pos);
break;
}
}
/*
* At this point, all required information are available. Now build the range.
* In the special case were the target type is the generic Number type instead
* than a more specialized type, the finest suitable type will be determined.
*/
if (Number.class.isAssignableFrom(elementType)) {
Class<? extends Number> type = (Class) elementType;
Number min = (Number) minValue;
Number max = (Number) maxValue;
if (type == Number.class) {
type = Numbers.widestClass(Numbers.narrowestClass(min), Numbers.narrowestClass(max));
min = Numbers.cast(min, type);
max = Numbers.cast(max, type);
}
if (min != null && min.doubleValue() == Double.NEGATIVE_INFINITY) min = null;
if (max != null && max.doubleValue() == Double.POSITIVE_INFINITY) max = null;
if (unit != null) {
final MeasurementRange<?> range = new MeasurementRange(type, min, isMinIncluded, max, isMaxIncluded, unit);
return range;
}
return new NumberRange(type, min, isMinIncluded, max, isMaxIncluded);
} else if (Date.class.isAssignableFrom(elementType)) {
return new Range(Date.class, (Date) minValue, isMinIncluded, (Date) maxValue, isMaxIncluded);
} else {
return new Range(elementType,
(Comparable<?>) minValue, isMinIncluded,
(Comparable<?>) maxValue, isMaxIncluded);
}
}
/**
* Converts the given value to the a {@link #elementType} type.
*/
@SuppressWarnings("unchecked")
private Object convert(final Object value) throws UnconvertibleObjectException {
if (value == null || elementType.isInstance(value)) {
return value;
}
if (value instanceof Number && Number.class.isAssignableFrom(elementType)) {
return Numbers.cast((Number) value, (Class<? extends Number>) elementType);
}
throw new UnconvertibleObjectException(Errors.format(
Errors.Keys.IllegalClass_2, elementType, value.getClass()));
}
/**
* Returns a "nil" value. This is used for creating empty ranges.
*/
private Object valueOfNil() {
Object value = Numbers.valueOfNil(elementType);
if (value == null) {
if (Date.class.isAssignableFrom(elementType)) {
value = new Date();
} else {
value = 0;
}
}
return convert(value);
}
/**
* Returns a clone of this range format.
*
* @return a clone of this range format.
*/
@Override
public RangeFormat clone() {
final RangeFormat f = (RangeFormat) super.clone();
try {
AccessController.doPrivileged(new FinalFieldSetter<>(RangeFormat.class, "elementFormat", "unitFormat"))
.set(f, elementFormat.clone(), unitFormat.clone());
} catch (ReflectiveOperationException e) {
throw FinalFieldSetter.cloneFailure(e);
}
return f;
}
}