TemporalFields.java

/*
 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  * Neither the name of JSR-310 nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.threeten.extra;

import static java.time.temporal.ChronoField.DAY_OF_YEAR;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.YEAR;
import static java.time.temporal.ChronoUnit.DAYS;
import static java.time.temporal.ChronoUnit.YEARS;
import static java.time.temporal.IsoFields.QUARTER_OF_YEAR;
import static java.time.temporal.IsoFields.QUARTER_YEARS;

import java.time.DateTimeException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.IsoChronology;
import java.time.format.ResolverStyle;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalUnit;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.time.temporal.ValueRange;
import java.util.Map;

/**
 * Additional Temporal fields.
 * <p>
 * This provides additional fields and units not in the JDK.
 */
public final class TemporalFields {

    /**
     * The field that represents the day-of-half.
     * <p>
     * This field allows the day-of-half value to be queried and set.
     * The day-of-half has values from 1 to 181 in H1 of a standard year, from 1 to 182
     * in H1 of a leap year and from 1 to 184 in H2.
     * <p>
     * The day-of-half can only be calculated if the day-of-year, month-of-year and year
     * are available.
     * <p>
     * When setting this field, the value is allowed to be partially lenient, taking any
     * value from 1 to 184. If the half has less than 184 days, then the day will end up
     * in the following half-year.
     * <p>
     * In the resolving phase of parsing, a date can be created from a year,
     * half-of-year and day-of-half.
     * <p>
     * In {@linkplain ResolverStyle#STRICT strict mode}, all three fields are
     * validated against their range of valid values. The day-of-half field
     * is validated from 1 to 181, 182 or 184 depending on the year and half.
     * <p>
     * In {@linkplain ResolverStyle#SMART smart mode}, all three fields are
     * validated against their range of valid values. The day-of-half field is
     * validated between 1 and 184, ignoring the actual range based on the year and half.
     * If the day-of-half exceeds the actual range, then the resulting date is in the next half-year.
     * <p>
     * In {@linkplain ResolverStyle#LENIENT lenient mode}, only the year is validated
     * against the range of valid values. The resulting date is calculated equivalent to
     * the following three stage approach. First, create a date on the first of January
     * in the requested year. Then take the half-of-year, subtract one, and add the
     * amount in halves to the date. Finally, take the day-of-half, subtract one,
     * and add the amount in days to the date.
     * <p>
     * This unit is an immutable and thread-safe singleton.
     */
    public static final TemporalField DAY_OF_HALF = DayOfHalfField.INSTANCE;
    /**
     * The field that represents the half-of-year.
     * <p>
     * This field allows the half-of-year value to be queried and set.
     * The half-of-year has values from 1 to 2.
     * <p>
     * The half-of-year can only be calculated if the month-of-year is available.
     * <p>
     * In the resolving phase of parsing, a date can be created from a year,
     * half-of-year and day-of-half.
     * See {@link #DAY_OF_HALF} for details.
     * <p>
     * This unit is an immutable and thread-safe singleton.
     */
    public static final TemporalField HALF_OF_YEAR = HalfOfYearField.INSTANCE;
    /**
     * Unit that represents the concept of a half-year.
     * For the ISO calendar system, it is equal to 6 months.
     * The estimated duration of a half-year is one half of {@code 365.2425 Days}.
     * <p>
     * This unit is an immutable and thread-safe singleton.
     */
    public static final TemporalUnit HALF_YEARS = HalfUnit.INSTANCE;

    /**
     * Restricted constructor.
     */
    private TemporalFields() {
    }

    //-------------------------------------------------------------------------
    /**
     * Implementation of day-of-half.
     */
    private static enum DayOfHalfField implements TemporalField {
        INSTANCE;

        private static final ValueRange RANGE = ValueRange.of(1, 181, 184);
        private static final long serialVersionUID = 262362728L;

        //-----------------------------------------------------------------------
        @Override
        public TemporalUnit getBaseUnit() {
            return DAYS;
        }

        @Override
        public TemporalUnit getRangeUnit() {
            return HALF_YEARS;
        }

        @Override
        public boolean isDateBased() {
            return true;
        }

        @Override
        public boolean isTimeBased() {
            return false;
        }

        @Override
        public ValueRange range() {
            return RANGE;
        }

        //-----------------------------------------------------------------------
        @Override
        public boolean isSupportedBy(TemporalAccessor temporal) {
            return temporal.isSupported(DAY_OF_YEAR) &&
                    temporal.isSupported(MONTH_OF_YEAR) &&
                    temporal.isSupported(YEAR);
        }

        @Override
        public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
            if (!temporal.isSupported(this)) {
                throw new DateTimeException("Unsupported field: DayOfHalf");
            }
            long hoy = temporal.getLong(HALF_OF_YEAR);
            if (hoy == 1) {
                long year = temporal.getLong(YEAR);
                return (IsoChronology.INSTANCE.isLeapYear(year) ? ValueRange.of(1, 182) : ValueRange.of(1, 181));
            } else if (hoy == 2) {
                return ValueRange.of(1, 184);
            } // else value not from 1 to 2, so drop through
            return range();
        }

        @Override
        public long getFrom(TemporalAccessor temporal) {
            if (isSupportedBy(temporal) == false) {
                throw new UnsupportedTemporalTypeException("Unsupported field: DayOfHalf");
            }
            int doy = temporal.get(DAY_OF_YEAR);
            int moy = temporal.get(MONTH_OF_YEAR);
            long year = temporal.getLong(YEAR);
            return moy <= 6 ? doy : doy - 181 - (IsoChronology.INSTANCE.isLeapYear(year) ? 1 : 0);
        }

        @SuppressWarnings("unchecked")
        @Override
        public <R extends Temporal> R adjustInto(R temporal, long newValue) {
            // calls getFrom() to check if supported
            long curValue = getFrom(temporal);
            range().checkValidValue(newValue, this);  // leniently check from 1 to 184
            return (R) temporal.with(DAY_OF_YEAR, temporal.getLong(DAY_OF_YEAR) + (newValue - curValue));
        }

        //-----------------------------------------------------------------------
        @Override
        public ChronoLocalDate resolve(
                Map<TemporalField, Long> fieldValues,
                TemporalAccessor partialTemporal,
                ResolverStyle resolverStyle) {

            Long yearLong = fieldValues.get(YEAR);
            Long hoyLong = fieldValues.get(HALF_OF_YEAR);
            if (yearLong == null || hoyLong == null) {
                return null;
            }
            int y = YEAR.checkValidIntValue(yearLong);  // always validate
            long doh = fieldValues.get(DAY_OF_HALF);
            LocalDate date;
            if (resolverStyle == ResolverStyle.LENIENT) {
                date = LocalDate.of(y, 1, 1).plusMonths(Math.multiplyExact(Math.subtractExact(hoyLong, 1), 6));
                doh = Math.subtractExact(doh, 1);
            } else {
                int qoy = HALF_OF_YEAR.range().checkValidIntValue(hoyLong, HALF_OF_YEAR);  // validated
                date = LocalDate.of(y, ((qoy - 1) * 6) + 1, 1);
                if (doh < 1 || doh > 181) {
                    if (resolverStyle == ResolverStyle.STRICT) {
                        rangeRefinedBy(date).checkValidValue(doh, this);  // only allow exact range
                    } else {  // SMART
                        range().checkValidValue(doh, this);  // allow 1-184 rolling into next quarter
                    }
                }
                doh--;
            }
            fieldValues.remove(this);
            fieldValues.remove(YEAR);
            fieldValues.remove(HALF_OF_YEAR);
            return date.plusDays(doh);
        }

        //-----------------------------------------------------------------------
        @Override
        public String toString() {
            return "DayOfHalf";
        }
    }

    //-------------------------------------------------------------------------
    /**
     * Implementation of half-of-year.
     */
    private static enum HalfOfYearField implements TemporalField {
        INSTANCE;

        private static final long serialVersionUID = -29115701L;

        //-----------------------------------------------------------------------
        @Override
        public TemporalUnit getBaseUnit() {
            return HALF_YEARS;
        }

        @Override
        public TemporalUnit getRangeUnit() {
            return YEARS;
        }

        @Override
        public boolean isDateBased() {
            return true;
        }

        @Override
        public boolean isTimeBased() {
            return false;
        }

        @Override
        public ValueRange range() {
            return ValueRange.of(1, 2);
        }

        @Override
        public boolean isSupportedBy(TemporalAccessor temporal) {
            return temporal.isSupported(QUARTER_OF_YEAR);
        }

        @Override
        public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
            return range();
        }

        @Override
        public long getFrom(TemporalAccessor temporal) {
            if (isSupportedBy(temporal) == false) {
                throw new UnsupportedTemporalTypeException("Unsupported field: HalfOfYear");
            }
            long qoy = temporal.get(QUARTER_OF_YEAR);
            return qoy <= 2 ? 1 : 2;
        }

        @SuppressWarnings("unchecked")
        @Override
        public <R extends Temporal> R adjustInto(R temporal, long newValue) {
            // calls getFrom() to check if supported
            long curValue = getFrom(temporal);
            range().checkValidValue(newValue, this);  // strictly check from 1 to 2
            return (R) temporal.with(MONTH_OF_YEAR, temporal.getLong(MONTH_OF_YEAR) + (newValue - curValue) * 6);
        }

        @Override
        public String toString() {
            return "HalfOfYear";
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Implementation of the half-year unit.
     */
    private static enum HalfUnit implements TemporalUnit {

        /**
         * Unit that represents the concept of a week-based-year.
         */
        INSTANCE;

        @Override
        public Duration getDuration() {
            return Duration.ofSeconds(31556952L / 2);
        }

        @Override
        public boolean isDurationEstimated() {
            return true;
        }

        @Override
        public boolean isDateBased() {
            return true;
        }

        @Override
        public boolean isTimeBased() {
            return false;
        }

        @Override
        public boolean isSupportedBy(Temporal temporal) {
            return temporal.isSupported(QUARTER_OF_YEAR);
        }

        @SuppressWarnings("unchecked")
        @Override
        public <R extends Temporal> R addTo(R temporal, long amount) {
            return (R) temporal.plus(Math.multiplyExact(amount, 2), QUARTER_YEARS);
        }

        @Override
        public long between(Temporal temporal1Inclusive, Temporal temporal2Exclusive) {
            if (temporal1Inclusive.getClass() != temporal2Exclusive.getClass()) {
                return temporal1Inclusive.until(temporal2Exclusive, this);
            }
            return temporal1Inclusive.until(temporal2Exclusive, QUARTER_YEARS) / 2;
        }

        @Override
        public String toString() {
            return "HalfYears";
        }
    }

}