ConstrainedFieldPosition.java

// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.text;

import java.text.Format.Field;
import java.util.Objects;

/**
 * Represents a span of a string containing a given field.
 *
 * <p>This class differs from FieldPosition in the following ways:
 *
 * <p>1. It has information on the field category. 2. It allows you to set constraints to use when
 * iterating over field positions. 3. It is used for the newer FormattedValue APIs.
 *
 * @author sffc
 * @stable ICU 64
 */
public class ConstrainedFieldPosition {

    /**
     * Represents the type of constraint for ConstrainedFieldPosition.
     *
     * <p>Constraints are used to control the behavior of iteration in FormattedValue.
     */
    private enum ConstraintType {
        /**
         * Represents the lack of a constraint.
         *
         * <p>This is the value of fConstraint if no "constrain" methods were called.
         */
        NONE,

        /**
         * Represents that the field class is constrained.
         *
         * <p>This is the value of fConstraint after {@link #constrainClass} is called.
         *
         * <p>FormattedValue implementations should not change the field class when this constraint
         * is active.
         */
        CLASS,

        /**
         * Represents that the field is constrained.
         *
         * <p>This is the value of fConstraint after {@link #constrainField} is called.
         *
         * <p>FormattedValue implementations should not change the field when this constraint is
         * active.
         */
        FIELD,

        /**
         * Represents that the field value is constrained.
         *
         * <p>This is the value of fConstraint after {@link #constrainField} is called.
         *
         * <p>FormattedValue implementations should not change the field or value with this
         * constraint.
         */
        VALUE
    };

    private ConstraintType fConstraint;
    private Class<?> fClassConstraint;
    private Field fField;
    private Object fValue;
    private int fStart;
    private int fLimit;
    private long fContext;

    /**
     * Initializes a CategoryFieldPosition.
     *
     * <p>By default, the CategoryFieldPosition has no iteration constraints.
     *
     * @stable ICU 64
     */
    public ConstrainedFieldPosition() {
        reset();
    }

    /**
     * Resets this ConstrainedFieldPosition to its initial state, as if it were newly created:
     *
     * <p>- Removes any constraints that may have been set on the instance. - Resets the iteration
     * position.
     *
     * @stable ICU 64
     */
    public void reset() {
        fConstraint = ConstraintType.NONE;
        fClassConstraint = Object.class;
        fField = null;
        fValue = null;
        fStart = 0;
        fLimit = 0;
        fContext = 0;
    }

    /**
     * Sets a constraint on the field.
     *
     * <p>When this instance of ConstrainedFieldPosition is passed to {@link
     * FormattedValue#nextPosition}, positions are skipped unless they have the given field.
     *
     * <p>Any previously set constraints are cleared.
     *
     * <p>For example, to loop over all grouping separators:
     *
     * <pre>
     * ConstrainedFieldPosition cfpos;
     * cfpos.constrainField(NumberFormat.Field.GROUPING_SEPARATOR);
     * while (fmtval.nextPosition(cfpos)) {
     *   // handle the grouping separator position
     * }
     * </pre>
     *
     * Changing the constraint while in the middle of iterating over a FormattedValue does not
     * generally have well-defined behavior.
     *
     * @param field The field to fix when iterating.
     * @stable ICU 64
     */
    public void constrainField(Field field) {
        if (field == null) {
            throw new IllegalArgumentException("Cannot constrain on null field");
        }
        fConstraint = ConstraintType.FIELD;
        fClassConstraint = Object.class;
        fField = field;
        fValue = null;
    }

    /**
     * Sets a constraint on the field class.
     *
     * <p>When this instance of ConstrainedFieldPosition is passed to {@link
     * FormattedValue#nextPosition}, positions are skipped unless the field is an instance of the
     * class constraint, including subclasses.
     *
     * <p>Any previously set constraints are cleared.
     *
     * <p>For example, to loop over only the number-related fields:
     *
     * <pre>
     * ConstrainedFieldPosition cfpos;
     * cfpos.constrainClass(NumberFormat.Field.class);
     * while (fmtval.nextPosition(cfpos)) {
     *   // handle the number-related field position
     * }
     * </pre>
     *
     * @param classConstraint The field class to fix when iterating.
     * @stable ICU 64
     */
    public void constrainClass(Class<?> classConstraint) {
        if (classConstraint == null) {
            throw new IllegalArgumentException("Cannot constrain on null field class");
        }
        fConstraint = ConstraintType.CLASS;
        fClassConstraint = classConstraint;
        fField = null;
        fValue = null;
    }

    /**
     * Sets a constraint on field and field value.
     *
     * <p>When this instance of ConstrainedFieldPosition is passed to {@link
     * FormattedValue#nextPosition}, positions are skipped unless both the field and the field value
     * are equal.
     *
     * <p>Any previously set constraints are cleared.
     *
     * <p>For example, to find the span a date interval corresponding to the first date:
     *
     * <pre>
     * ConstrainedFieldPosition cfpos;
     * cfpos.constrainFieldAndValue(DateIntervalFormat.SpanField.DATE_INTERVAL_SPAN, 0);
     * while (fmtval.nextPosition(cfpos)) {
     *   // handle the span of the first date in the date interval
     * }
     * </pre>
     *
     * @param field The field to fix when iterating.
     * @param fieldValue The field value to fix when iterating.
     * @internal ICU 64 technology preview
     * @deprecated This API is for technology preview and might be changed or removed in a future
     *     release.
     */
    @Deprecated
    public void constrainFieldAndValue(Field field, Object fieldValue) {
        fConstraint = ConstraintType.VALUE;
        fClassConstraint = Object.class;
        fField = field;
        fValue = fieldValue;
    }

    /**
     * Gets the field for the current position.
     *
     * <p>The return value is well-defined and non-null only after FormattedValue#nextPosition
     * returns true.
     *
     * @return The field saved in the instance. See above for null conditions.
     * @stable ICU 64
     */
    public Field getField() {
        return fField;
    }

    /**
     * Gets the INCLUSIVE start index for the current position.
     *
     * <p>The return value is well-defined only after FormattedValue#nextPosition returns true.
     *
     * @return The start index saved in the instance.
     * @stable ICU 64
     */
    public int getStart() {
        return fStart;
    }

    /**
     * Gets the EXCLUSIVE end index stored for the current position.
     *
     * <p>The return value is well-defined only after FormattedValue#nextPosition returns true.
     *
     * @return The end index saved in the instance.
     * @stable ICU 64
     */
    public int getLimit() {
        return fLimit;
    }

    /**
     * Gets the value associated with the current field position. The field value is often not set.
     *
     * <p>The return value is well-defined only after FormattedValue#nextPosition returns true.
     *
     * @return The value for the current position. Might be null.
     * @stable ICU 64
     */
    public Object getFieldValue() {
        return fValue;
    }

    /**
     * Gets an int64 that FormattedValue implementations may use for storage.
     *
     * <p>The initial value is zero.
     *
     * <p>Users of FormattedValue should not need to call this method.
     *
     * @return The current iteration context from {@link #setInt64IterationContext}.
     * @stable ICU 64
     */
    public long getInt64IterationContext() {
        return fContext;
    }

    /**
     * Sets an int64 that FormattedValue implementations may use for storage.
     *
     * <p>Intended to be used by FormattedValue implementations.
     *
     * @param context The new iteration context.
     * @stable ICU 64
     */
    public void setInt64IterationContext(long context) {
        fContext = context;
    }

    /**
     * Sets new values for the primary public getters.
     *
     * <p>Intended to be used by FormattedValue implementations.
     *
     * <p>It is up to the implementation to ensure that the user-requested constraints are
     * satisfied. This method does not check!
     *
     * @param field The new field.
     * @param value The new field value. Should be null if there is no value.
     * @param start The new inclusive start index.
     * @param limit The new exclusive end index.
     * @stable ICU 64
     */
    public void setState(Field field, Object value, int start, int limit) {
        // Check matchesField only as an assertion (debug build)
        if (field != null) {
            assert matchesField(field, value);
        }

        fField = field;
        fValue = value;
        fStart = start;
        fLimit = limit;
    }

    /**
     * Determines whether a given field and value should be included given the constraints.
     *
     * <p>Intended to be used by FormattedValue implementations.
     *
     * @param field The field to test.
     * @param fieldValue The field value to test. Should be null if there is no value.
     * @return Whether the field should be included given the constraints.
     * @stable ICU 64
     */
    public boolean matchesField(Field field, Object fieldValue) {
        if (field == null) {
            throw new IllegalArgumentException("field must not be null");
        }
        switch (fConstraint) {
            case NONE:
                return true;
            case CLASS:
                return fClassConstraint.isAssignableFrom(field.getClass());
            case FIELD:
                return fField == field;
            case VALUE:
                // Note: Objects.equals is Android API level 19 and Java 1.7
                return fField == field && Objects.equals(fValue, fieldValue);
            default:
                throw new AssertionError();
        }
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 64
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("CFPos[");
        sb.append(fStart);
        sb.append('-');
        sb.append(fLimit);
        sb.append(' ');
        sb.append(fField);
        sb.append(']');
        return sb.toString();
    }
}