| /* |
| * 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.util.Objects; |
| 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.time.Instant; |
| import java.time.format.FormatStyle; |
| import java.time.format.DateTimeFormatterBuilder; |
| import java.time.temporal.Temporal; |
| import java.lang.reflect.InaccessibleObjectException; |
| import javax.measure.Unit; |
| import org.apache.sis.util.Numbers; |
| import org.apache.sis.util.Localized; |
| import org.apache.sis.util.UnconvertibleObjectException; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.privy.LocalizedParseException; |
| import org.apache.sis.util.privy.TemporalDate; |
| import org.apache.sis.util.privy.Numerics; |
| |
| |
| /** |
| * Parses and formats {@link Range} instances according the given locale. |
| * This class complies to the format described in the <a href="https://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 of 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 equal 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 (<dfn>closed interval</dfn>);</li> |
| * <li>"{@code (min … max)}" if both endpoints are exclusive (<dfn>open interval</dfn>);</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. |
| * |
| * <h2>Lenient parsing</h2> |
| * 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> |
| * |
| * <h2>Range type and type of range elements</h2> |
| * 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.4 |
| * |
| * @see Range#toString() |
| * @see <a href="https://en.wikipedia.org/wiki/ISO_31-11">Wikipedia: ISO 31-11</a> |
| * |
| * @since 0.3 |
| */ |
| 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 |
| */ |
| 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>'{'</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 sometimes 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>'}'</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 sometimes 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 dates 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 { |
| this.locale = Objects.requireNonNull(locale); |
| this.elementType = Objects.requireNonNull(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) || elementType == Instant.class) { |
| elementFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale); |
| unitFormat = null; |
| } else if (Temporal.class.isAssignableFrom(elementType)) { |
| final FormatStyle dateStyle = TemporalDate.hasDateFields(elementType) ? FormatStyle.SHORT : null; |
| final FormatStyle timeStyle = TemporalDate.hasTimeFields(elementType) ? FormatStyle.SHORT : null; |
| elementFormat = new DateTimeFormatterBuilder().appendLocalized(dateStyle, timeStyle).toFormatter(locale).toFormat(); |
| 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. This is used for validating argument |
| * of {@link Object} type in formatting methods. |
| */ |
| 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 cannot 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 = toFormattable(minValue); break; |
| case MAX_VALUE_FIELD: value = toFormattable(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: |
| * |
| * {@snippet lang="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 cannot 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 cannot 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 latter case is an extension to the standard format since empty |
| * ranges are usually represented by {} instead of []. |
| */ |
| 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.startsWith(infinity, index)) { |
| 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.startsWith(separator, index)) { |
| 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.startsWith(infinity, index)) { |
| 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 cannot 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 an instance of the {@link #elementType} type. |
| * This method is partially the converse of {@link #toFormattable(Object)}. |
| */ |
| @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); |
| } |
| if (value instanceof Date && elementType == Instant.class) { |
| return ((Date) value).toInstant(); |
| } |
| throw new UnconvertibleObjectException(Errors.format( |
| Errors.Keys.IllegalClass_2, elementType, value.getClass())); |
| } |
| |
| /** |
| * Converts the given object to a type that {@code format(…)} method can process. |
| * This method is partially the converse of {@link #convert(Object)}. |
| */ |
| private Object toFormattable(final Object value) { |
| if (value instanceof Instant && elementType == Instant.class) { |
| return Date.from((Instant) value); |
| } |
| return value; |
| } |
| |
| /** |
| * 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 { |
| f.clone("elementFormat"); |
| f.clone("unitFormat"); |
| } catch (ReflectiveOperationException e) { |
| throw (InaccessibleObjectException) new InaccessibleObjectException().initCause(e); |
| } |
| return f; |
| } |
| |
| /** |
| * Clones the value in the specified field. |
| */ |
| private void clone(final String field) throws ReflectiveOperationException { |
| final var f = RangeFormat.class.getDeclaredField(field); |
| f.setAccessible(true); |
| f.set(this, ((Format) f.get(this)).clone()); |
| } |
| } |