DiscordianDate.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.chrono;

import static java.time.temporal.ChronoField.ALIGNED_WEEK_OF_YEAR;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import static java.time.temporal.ChronoField.DAY_OF_YEAR;
import static java.time.temporal.ChronoField.EPOCH_DAY;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.YEAR;
import static org.threeten.extra.chrono.DiscordianChronology.DAYS_IN_MONTH;
import static org.threeten.extra.chrono.DiscordianChronology.DAYS_IN_WEEK;
import static org.threeten.extra.chrono.DiscordianChronology.MONTHS_IN_YEAR;
import static org.threeten.extra.chrono.DiscordianChronology.OFFSET_FROM_ISO_0000;
import static org.threeten.extra.chrono.DiscordianChronology.WEEKS_IN_YEAR;

import java.io.Serializable;
import java.time.Clock;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.ChronoLocalDateTime;
import java.time.chrono.ChronoPeriod;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalQuery;
import java.time.temporal.TemporalUnit;
import java.time.temporal.ValueRange;

/**
 * A date in the Discordian calendar system.
 * <p>
 * This date operates using the {@linkplain DiscordianChronology Discordian calendar}.
 * This calendar system is used by some adherents to Discordianism.
 * The Discordian differs from the Gregorian in terms of the length of the week and month, and uses an offset year.
 * Dates are aligned such that {@code 0001-01-01 (Discordian)} is {@code -1165-01-01 (ISO)}.
 *
 * <h3>Implementation Requirements</h3>
 * This class is immutable and thread-safe.
 * <p>
 * This class must be treated as a value type. Do not synchronize, rely on the
 * identity hash code or use the distinction between equals() and ==.
 */
public final class DiscordianDate
        extends AbstractDate
        implements ChronoLocalDate, Serializable {

    /**
     * Serialization version.
     */
    private static final long serialVersionUID = -4340508226506164852L;
    /**
     * The difference between the Discordian and ISO epoch day count (Discordian 1167-01-01 to ISO 1970-01-01).
     */
    private static final int DISCORDIAN_1167_TO_ISO_1970 = 719162;
    /**
     * The days per short 4 year cycle.
     */
    private static final int DAYS_PER_SHORT_CYCLE = (365 * 4) + 1;
    /**
     * The days per 100 year cycle.
     */
    private static final int DAYS_PER_CYCLE = (DAYS_PER_SHORT_CYCLE * 25) - 1;
    /**
     * The days per 400 year long cycle.
     */
    private static final int DAYS_PER_LONG_CYCLE = (DAYS_PER_CYCLE * 4) + 1;
    /**
     * Offset in days from start of year to St. Tib's Day.
     */
    private static final int ST_TIBS_OFFSET = 60;

    /**
     * The proleptic year.
     */
    private final int prolepticYear;
    /**
     * The month.
     */
    private final short month;
    /**
     * The day.
     */
    private final short day;

    //-----------------------------------------------------------------------
    /**
     * Obtains the current {@code DiscordianDate} from the system clock in the default time-zone.
     * <p>
     * This will query the {@link Clock#systemDefaultZone() system clock} in the default
     * time-zone to obtain the current date.
     * <p>
     * Using this method will prevent the ability to use an alternate clock for testing
     * because the clock is hard-coded.
     *
     * @return the current date using the system clock and default time-zone, not null
     */
    public static DiscordianDate now() {
        return now(Clock.systemDefaultZone());
    }

    /**
     * Obtains the current {@code DiscordianDate} from the system clock in the specified time-zone.
     * <p>
     * This will query the {@link Clock#system(ZoneId) system clock} to obtain the current date.
     * Specifying the time-zone avoids dependence on the default time-zone.
     * <p>
     * Using this method will prevent the ability to use an alternate clock for testing
     * because the clock is hard-coded.
     *
     * @param zone  the zone ID to use, not null
     * @return the current date using the system clock, not null
     */
    public static DiscordianDate now(ZoneId zone) {
        return now(Clock.system(zone));
    }

    /**
     * Obtains the current {@code DiscordianDate} from the specified clock.
     * <p>
     * This will query the specified clock to obtain the current date - today.
     * Using this method allows the use of an alternate clock for testing.
     * The alternate clock may be introduced using {@linkplain Clock dependency injection}.
     *
     * @param clock  the clock to use, not null
     * @return the current date, not null
     * @throws DateTimeException if the current date cannot be obtained
     */
    public static DiscordianDate now(Clock clock) {
        LocalDate now = LocalDate.now(clock);
        return DiscordianDate.ofEpochDay(now.toEpochDay());
    }

    /**
     * Obtains a {@code DiscordianDate} representing a date in the Discordian calendar
     * system from the proleptic-year, month-of-year and day-of-month fields.
     * <p>
     * This returns a {@code DiscordianDate} with the specified fields.
     * The day must be valid for the year and month, otherwise an exception will be thrown.
     * <p>
     * St. Tib's Day is indicated by specifying 0 for both month and day-of-month.
     *
     * @param prolepticYear  the Discordian proleptic-year
     * @param month  the Discordian month-of-year, from 1 to 5
     * @param dayOfMonth  the Discordian day-of-month, from 1 to 73
     * @return the date in Discordian calendar system, not null
     * @throws DateTimeException if the value of any field is out of range,
     *  or if the day-of-month is invalid for the month-year
     */
    public static DiscordianDate of(int prolepticYear, int month, int dayOfMonth) {
        return DiscordianDate.create(prolepticYear, month, dayOfMonth);
    }

    /**
     * Obtains a {@code DiscordianDate} from a temporal object.
     * <p>
     * This obtains a date in the Discordian calendar system based on the specified temporal.
     * A {@code TemporalAccessor} represents an arbitrary set of date and time information,
     * which this factory converts to an instance of {@code DiscordianDate}.
     * <p>
     * The conversion typically uses the {@link ChronoField#EPOCH_DAY EPOCH_DAY}
     * field, which is standardized across calendar systems.
     * <p>
     * This method matches the signature of the functional interface {@link TemporalQuery}
     * allowing it to be used as a query via method reference, {@code DiscordianDate::from}.
     *
     * @param temporal  the temporal object to convert, not null
     * @return the date in Discordian calendar system, not null
     * @throws DateTimeException if unable to convert to a {@code DiscordianDate}
     */
    public static DiscordianDate from(TemporalAccessor temporal) {
        if (temporal instanceof DiscordianDate) {
            return (DiscordianDate) temporal;
        }
        return DiscordianDate.ofEpochDay(temporal.getLong(EPOCH_DAY));
    }

    //-----------------------------------------------------------------------
    /**
     * Obtains a {@code DiscordianDate} representing a date in the Discordian calendar
     * system from the proleptic-year and day-of-year fields.
     * <p>
     * This returns a {@code DiscordianDate} with the specified fields.
     * The day must be valid for the year, otherwise an exception will be thrown.
     *
     * @param prolepticYear  the Discordian proleptic-year
     * @param dayOfYear  the Discordian day-of-year, from 1 to 366
     * @return the date in Discordian calendar system, not null
     * @throws DateTimeException if the value of any field is out of range,
     *  or if the day-of-year is invalid for the year
     */
    static DiscordianDate ofYearDay(int prolepticYear, int dayOfYear) {
        DiscordianChronology.YEAR_RANGE.checkValidValue(prolepticYear, YEAR);
        DAY_OF_YEAR.checkValidValue(dayOfYear);
        boolean leap = DiscordianChronology.INSTANCE.isLeapYear(prolepticYear);
        if (dayOfYear == 366 && !leap) {
            throw new DateTimeException("Invalid date 'DayOfYear 366' as '" + prolepticYear + "' is not a leap year");
        }

        if (leap) {
            if (dayOfYear == ST_TIBS_OFFSET) {
                // Take care of special case of St Tib's Day.
                return new DiscordianDate(prolepticYear, 0, 0);
            } else if (dayOfYear > ST_TIBS_OFFSET) {
                // Offset dayOfYear to account for added day.
                dayOfYear--;
            }
        }

        int month = (dayOfYear - 1) / DAYS_IN_MONTH + 1;
        int dayOfMonth = (dayOfYear - 1) % DAYS_IN_MONTH + 1;

        return new DiscordianDate(prolepticYear, month, dayOfMonth);
    }

    /**
     * Obtains a {@code DiscordianDate} representing a date in the Discordian calendar
     * system from the epoch-day.
     *
     * @param epochDay  the epoch day to convert based on 1970-01-01 (ISO)
     * @return the date in Discordian calendar system, not null
     * @throws DateTimeException if the epoch-day is out of range
     */
    static DiscordianDate ofEpochDay(final long epochDay) {
        DiscordianChronology.EPOCH_DAY_RANGE.checkValidValue(epochDay, EPOCH_DAY);

        // use of Discordian 1167 makes leap year at end of long cycle
        long discordianEpochDay = epochDay + DISCORDIAN_1167_TO_ISO_1970;

        long longCycle = Math.floorDiv(discordianEpochDay, DAYS_PER_LONG_CYCLE);
        long daysInLongCycle = Math.floorMod(discordianEpochDay, DAYS_PER_LONG_CYCLE);
        if (daysInLongCycle == DAYS_PER_LONG_CYCLE - 1) {
            int year = (int) (longCycle * 400) + 400;
            return ofYearDay(year + OFFSET_FROM_ISO_0000, 366);
        }

        int cycle = (int) daysInLongCycle / DAYS_PER_CYCLE;
        int dayInCycle = (int) daysInLongCycle % DAYS_PER_CYCLE;
        int shortCycle = dayInCycle / DAYS_PER_SHORT_CYCLE;
        int dayInShortCycle = dayInCycle % DAYS_PER_SHORT_CYCLE;

        if (dayInShortCycle == DAYS_PER_SHORT_CYCLE - 1) {
            int year = (int) (longCycle * 400) + (cycle * 100) + (shortCycle * 4) + 4;
            return ofYearDay(year + OFFSET_FROM_ISO_0000, 366);
        }

        int year = (int) (longCycle * 400) + (cycle * 100) + (shortCycle * 4) + (dayInShortCycle / 365) + 1;
        int dayOfYear = (dayInShortCycle % 365) + 1;

        return ofYearDay(year + OFFSET_FROM_ISO_0000, dayOfYear);
    }

    private static DiscordianDate resolvePreviousValid(int prolepticYear, int month, int day) {
        switch (month) {
            case 0:
                day = 0;
                if (DiscordianChronology.INSTANCE.isLeapYear(prolepticYear)) {
                    break;
                }
                month = 1;
                // fall through
            default:
                if (day == 0) {
                    day = ST_TIBS_OFFSET;
                }
        }
        return new DiscordianDate(prolepticYear, month, day);
    }

    private static long getLeapYearsBefore(long year) {
        long offsetYear = year - OFFSET_FROM_ISO_0000 - 1;
        return Math.floorDiv(offsetYear, 4) - Math.floorDiv(offsetYear, 100) + Math.floorDiv(offsetYear, 400);
    }

    /**
     * Creates a {@code DiscordianDate} validating the input.
     *
     * @param prolepticYear  the Discordian proleptic-year
     * @param month  the Discordian month-of-year, from 1 to 5
     * @param dayOfMonth  the Discordian day-of-month, from 1 to 73
     * @return the date in Discordian calendar system, not null
     * @throws DateTimeException if the value of any field is out of range,
     *  or if the day-of-year is invalid for the month-year
     */
    static DiscordianDate create(int prolepticYear, int month, int dayOfMonth) {
        DiscordianChronology.YEAR_RANGE.checkValidValue(prolepticYear, YEAR);
        DiscordianChronology.MONTH_OF_YEAR_RANGE.checkValidValue(month, MONTH_OF_YEAR);
        DiscordianChronology.DAY_OF_MONTH_RANGE.checkValidValue(dayOfMonth, DAY_OF_MONTH);

        if (month == 0 || dayOfMonth == 0) {
            if (month != 0 || dayOfMonth != 0) {
                throw new DateTimeException("Invalid date '" + month + " " + dayOfMonth + "' as St. Tib's Day is the only special day inserted in a non-existent month.");
            } else if (!DiscordianChronology.INSTANCE.isLeapYear(prolepticYear)) {
                throw new DateTimeException("Invalid date 'St. Tibs Day' as '" + prolepticYear + "' is not a leap year");
            }
        }

        return new DiscordianDate(prolepticYear, month, dayOfMonth);
    }

    //-----------------------------------------------------------------------
    /**
     * Creates an instance from validated data.
     *
     * @param prolepticYear  the Discordian proleptic-year
     * @param month  the Discordian month, from 1 to 5
     * @param dayOfMonth  the Discordian day-of-month, from 1 to 73
     */
    private DiscordianDate(int prolepticYear, int month, int dayOfMonth) {
        this.prolepticYear = prolepticYear;
        this.month = (short) month;
        this.day = (short) dayOfMonth;
    }

    /**
     * Validates the object.
     *
     * @return the resolved date, not null
     */
    private Object readResolve() {
        return DiscordianDate.create(prolepticYear, month, day);
    }

    //-----------------------------------------------------------------------
    @Override
    int getProlepticYear() {
        return prolepticYear;
    }

    @Override
    int getMonth() {
        return month;
    }

    @Override
    int getDayOfMonth() {
        return day;
    }

    @Override
    int getDayOfYear() {
        // St. Tib's Day isn't part of any month, but would be the 60th day of the year.
        if (month == 0 && day == 0) {
            return ST_TIBS_OFFSET;
        }
        int dayOfYear = (month - 1) * DAYS_IN_MONTH + day;
        // If after St. Tib's day, need to offset to account for it.
        return dayOfYear + (dayOfYear >= ST_TIBS_OFFSET && isLeapYear() ? 1 : 0);
    }

    @Override
    AbstractDate withDayOfYear(int value) {
        return plusDays(value - getDayOfYear());
    }

    @Override
    int lengthOfWeek() {
        return DAYS_IN_WEEK;
    }

    @Override
    int lengthOfYearInMonths() {
        return MONTHS_IN_YEAR;
    }

    @Override
    ValueRange rangeAlignedWeekOfMonth() {
        return month == 0 ? ValueRange.of(0, 0) : ValueRange.of(1, 15);
    }

    @Override
    DiscordianDate resolvePrevious(int newYear, int newMonth, int dayOfMonth) {
        return resolvePreviousValid(newYear, newMonth, dayOfMonth);
    }

    //-----------------------------------------------------------------------
    @Override
    public ValueRange range(TemporalField field) {
        if (field instanceof ChronoField) {
            if (isSupported(field)) {
                ChronoField f = (ChronoField) field;
                switch (f) {
                    case ALIGNED_DAY_OF_WEEK_IN_MONTH:
                        return month == 0 ? ValueRange.of(0, 0) : ValueRange.of(1, DAYS_IN_WEEK);
                    case ALIGNED_DAY_OF_WEEK_IN_YEAR:
                        return ValueRange.of(isLeapYear() ? 0 : 1, DAYS_IN_WEEK);
                    case ALIGNED_WEEK_OF_YEAR:
                        return ValueRange.of(isLeapYear() ? 0 : 1, WEEKS_IN_YEAR);
                    case DAY_OF_MONTH:
                        return month == 0 ? ValueRange.of(0, 0) : ValueRange.of(1, DAYS_IN_MONTH);
                    case DAY_OF_WEEK:
                        return month == 0 ? ValueRange.of(0, 0) : ValueRange.of(1, DAYS_IN_WEEK);
                    case MONTH_OF_YEAR:
                        return ValueRange.of(isLeapYear() ? 0 : 1, MONTHS_IN_YEAR);
                    default:
                        break;
                }
            }
        }
        return super.range(field);
    }

    //-----------------------------------------------------------------------
    @Override
    public long getLong(TemporalField field) {
        if (field instanceof ChronoField) {
            switch ((ChronoField) field) {
                case ALIGNED_DAY_OF_WEEK_IN_MONTH:
                    return month == 0 ? 0 : super.getLong(field);
                case ALIGNED_DAY_OF_WEEK_IN_YEAR:
                    return getDayOfWeek();
                case ALIGNED_WEEK_OF_MONTH:
                    return month == 0 ? 0 : super.getLong(field);
                case ALIGNED_WEEK_OF_YEAR:
                    if (month == 0) {
                        return 0;
                    } else {
                        return ((getDayOfYear() - (getDayOfYear() >= ST_TIBS_OFFSET && isLeapYear() ? 1 : 0) - 1) / DAYS_IN_WEEK) + 1;
                    }
                default:
                    break;
            }
        }
        return super.getLong(field);
    }

    @Override
    int getDayOfWeek() {
        if (month == 0) {
            return 0;
        }
        // Need to offset to account for added day.
        int dayOfYear = getDayOfYear() - (getDayOfYear() >= ST_TIBS_OFFSET && isLeapYear() ? 1 : 0);
        return (dayOfYear - 1) % DAYS_IN_WEEK + 1;
    }

    @Override
    long getProlepticMonth() {
        // Consider St. Tib's day to be part of the 1st month for this count.
        return prolepticYear * MONTHS_IN_YEAR + (month == 0 ? 1 : month) - 1;
    }

    long getProlepticWeek() {
        // Consider St. Tib's day to be part of the 12th week for this count.
        return ((long) prolepticYear) * WEEKS_IN_YEAR + (month == 0 ? ST_TIBS_OFFSET / DAYS_IN_WEEK : getLong(ALIGNED_WEEK_OF_YEAR)) - 1;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the chronology of this date, which is the Discordian calendar system.
     * <p>
     * The {@code Chronology} represents the calendar system in use.
     * The era and other fields in {@link ChronoField} are defined by the chronology.
     *
     * @return the Discordian chronology, not null
     */
    @Override
    public DiscordianChronology getChronology() {
        return DiscordianChronology.INSTANCE;
    }

    /**
     * Gets the era applicable at this date.
     * <p>
     * The Discordian calendar system has one era, 'YOLD',
     * defined by {@link DiscordianEra}.
     *
     * @return the era YOLD, not null
     */
    @Override
    public DiscordianEra getEra() {
        return DiscordianEra.YOLD;
    }

    @Override
    public int lengthOfMonth() {
        return month == 0 ? 1 : DAYS_IN_MONTH;
    }

    //-------------------------------------------------------------------------
    @Override
    public DiscordianDate with(TemporalAdjuster adjuster) {
        return (DiscordianDate) adjuster.adjustInto(this);
    }

    @Override
    public DiscordianDate with(TemporalField field, long newValue) {
        if (field instanceof ChronoField) {
            ChronoField f = (ChronoField) field;
            DiscordianChronology.INSTANCE.range(f).checkValidValue(newValue, f);
            int nvalue = (int) newValue;
            // trying to move to St Tibs
            if (nvalue == 0 && isLeapYear()) {
                switch (f) {
                    case DAY_OF_WEEK:
                    case ALIGNED_DAY_OF_WEEK_IN_MONTH:
                    case ALIGNED_DAY_OF_WEEK_IN_YEAR:
                    case ALIGNED_WEEK_OF_MONTH:
                    case ALIGNED_WEEK_OF_YEAR:
                    case DAY_OF_MONTH:
                    case MONTH_OF_YEAR:
                        if (month == 0) {
                            return this;
                        }
                        return DiscordianDate.create(prolepticYear, 0, 0);
                    default:
                        break;
                }
            }
            // currently on St Tibs
            if (month == 0) {
                switch (f) {
                    case YEAR:
                    case YEAR_OF_ERA:
                        if (DiscordianChronology.INSTANCE.isLeapYear(nvalue)) {
                            return DiscordianDate.create(nvalue, 0, 0);
                        }
                        // fall through
                    default:
                        return DiscordianDate.create(prolepticYear, 1, 60).with(field, newValue);
                }
            }
            // validate range (generally excluding zero value)
            range(f).checkValidValue(newValue, f);
            switch (f) {
                case DAY_OF_WEEK:
                case ALIGNED_DAY_OF_WEEK_IN_MONTH:
                case ALIGNED_DAY_OF_WEEK_IN_YEAR:
                    if (month == 1
                            && day >= ST_TIBS_OFFSET - DAYS_IN_WEEK + 1 && day < ST_TIBS_OFFSET + 1
                            && isLeapYear()) {
                        int currentDayOfWeek = getDayOfWeek();
                        // St. Tib's Day is between the 4th and 5th days of the week...
                        if (currentDayOfWeek < DAYS_IN_WEEK && nvalue == DAYS_IN_WEEK) {
                            return (DiscordianDate) plusDays(nvalue - currentDayOfWeek + 1);
                        } else if (currentDayOfWeek == DAYS_IN_WEEK && nvalue < DAYS_IN_WEEK) {
                            return (DiscordianDate) plusDays(nvalue - currentDayOfWeek - 1);
                        }
                    }
                    break;
                case ALIGNED_WEEK_OF_MONTH:
                case ALIGNED_WEEK_OF_YEAR:
                    if ((month == 1 || field == ALIGNED_WEEK_OF_YEAR) && isLeapYear()) {
                        // St. Tib's Day is in the middle of the 12th week...
                        int alignedWeek = (int) getLong(field);
                        int currentDayOfWeek = getDayOfWeek();
                        if ((alignedWeek > ST_TIBS_OFFSET / DAYS_IN_WEEK || (alignedWeek == ST_TIBS_OFFSET / DAYS_IN_WEEK && currentDayOfWeek == DAYS_IN_WEEK))
                                && (nvalue < ST_TIBS_OFFSET / DAYS_IN_WEEK || (nvalue == ST_TIBS_OFFSET / DAYS_IN_WEEK && currentDayOfWeek < DAYS_IN_WEEK))) {
                            return (DiscordianDate) plusDays((newValue - alignedWeek) * DAYS_IN_WEEK - 1);
                        } else if ((nvalue > ST_TIBS_OFFSET / DAYS_IN_WEEK || (nvalue == ST_TIBS_OFFSET / DAYS_IN_WEEK && currentDayOfWeek == DAYS_IN_WEEK))
                                && (alignedWeek < ST_TIBS_OFFSET / DAYS_IN_WEEK || (alignedWeek == ST_TIBS_OFFSET / DAYS_IN_WEEK && currentDayOfWeek < DAYS_IN_WEEK))) {
                            return (DiscordianDate) plusDays((newValue - alignedWeek) * lengthOfWeek() + 1);
                        }
                    }
                    break;
                default:
                    break;
            }
        }
        return (DiscordianDate) super.with(field, newValue);
    }

    //-----------------------------------------------------------------------
    @Override
    public DiscordianDate plus(TemporalAmount amount) {
        return (DiscordianDate) amount.addTo(this);
    }

    @Override
    public DiscordianDate plus(long amountToAdd, TemporalUnit unit) {
        if (unit instanceof ChronoUnit) {
            ChronoUnit f = (ChronoUnit) unit;
            switch (f) {
                case WEEKS:
                    return plusWeeks(amountToAdd);
                case MONTHS:
                    return plusMonths(amountToAdd);
                default:
                    break;
            }
        }
        return (DiscordianDate) super.plus(amountToAdd, unit);
    }

    @Override
    DiscordianDate plusMonths(long months) {
        if (months == 0) {
            return this;
        }
        long calcEm = Math.addExact(getProlepticMonth(), months);
        int newYear = Math.toIntExact(Math.floorDiv(calcEm, MONTHS_IN_YEAR));
        int newMonth = (int) (Math.floorMod(calcEm, MONTHS_IN_YEAR) + 1);
        // If the starting date was St. Tib's Day, attempt to retain that status.
        if (month == 0 && newMonth == 1) {
            newMonth = 0;
        }
        return resolvePrevious(newYear, newMonth, day);
    }

    @Override
    DiscordianDate plusWeeks(long weeks) {
        if (weeks == 0) {
            return this;
        }
        long calcEm = Math.addExact(getProlepticWeek(), weeks);
        int newYear = Math.toIntExact(Math.floorDiv(calcEm, WEEKS_IN_YEAR));
        // Give St. Tib's Day the same day-of-week as the day after it.
        int newDayOfYear = (int) (Math.floorMod(calcEm, WEEKS_IN_YEAR)) * DAYS_IN_WEEK + (month == 0 ? DAYS_IN_WEEK : get(DAY_OF_WEEK));
        // Need to offset day-of-year if leap year, and not heading to St. Tib's Day again.
        if (DiscordianChronology.INSTANCE.isLeapYear(newYear)
                && (newDayOfYear > ST_TIBS_OFFSET || (newDayOfYear == ST_TIBS_OFFSET && month != 0))) {
            newDayOfYear++;
        }
        return ofYearDay(newYear, newDayOfYear);
    }

    @Override
    public DiscordianDate minus(TemporalAmount amount) {
        return (DiscordianDate) amount.subtractFrom(this);
    }

    @Override
    public DiscordianDate minus(long amountToSubtract, TemporalUnit unit) {
        return (amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) : plus(-amountToSubtract, unit));
    }

    //-------------------------------------------------------------------------
    @Override  // for covariant return type
    @SuppressWarnings("unchecked")
    public ChronoLocalDateTime<DiscordianDate> atTime(LocalTime localTime) {
        return (ChronoLocalDateTime<DiscordianDate>) super.atTime(localTime);
    }

    @Override
    public long until(Temporal endExclusive, TemporalUnit unit) {
        return until(DiscordianDate.from(endExclusive), unit);
    }

    @Override
    long until(AbstractDate end, TemporalUnit unit) {
        if (unit instanceof ChronoUnit) {
            switch ((ChronoUnit) unit) {
                case WEEKS:
                    return weeksUntil(DiscordianDate.from(end));
                default:
                    break;
            }
        }
        return super.until(end, unit);
    }

    long weeksUntil(DiscordianDate end) {
        long weekStart = this.getProlepticWeek() * 8L;
        long weekEnd = end.getProlepticWeek() * 8L;
        // Toggle offset for St. Tib's Day based on the direction traveled.
        long packed1 = weekStart + (this.month == 0 && end.month != 0 ? (weekEnd > weekStart ? DAYS_IN_WEEK : DAYS_IN_WEEK - 1) : this.getDayOfWeek());  // no overflow
        long packed2 = weekEnd + (end.month == 0 && this.month != 0 ? (weekStart > weekEnd ? DAYS_IN_WEEK : DAYS_IN_WEEK - 1) : end.getDayOfWeek());  // no overflow
        return (packed2 - packed1) / 8L;
    }

    @Override
    long monthsUntil(AbstractDate end) {
        DiscordianDate discordianEnd = DiscordianDate.from(end);
        long monthStart = this.getProlepticMonth() * 128L;
        long monthEnd = discordianEnd.getProlepticMonth() * 128L;
        // Toggle offset for St. Tib's Day based on the direction traveled.
        long packed1 = monthStart + (this.month == 0 && discordianEnd.month != 0 ? (monthEnd > monthStart ? ST_TIBS_OFFSET : ST_TIBS_OFFSET - 1) : this.getDayOfMonth());  // no overflow
        long packed2 = monthEnd + (discordianEnd.month == 0 && this.month != 0 ? (monthStart > monthEnd ? ST_TIBS_OFFSET : ST_TIBS_OFFSET - 1) : end.getDayOfMonth());  // no overflow
        return (packed2 - packed1) / 128L;
    }

    @Override
    public ChronoPeriod until(ChronoLocalDate endDateExclusive) {
        long monthsUntil = monthsUntil(DiscordianDate.from(endDateExclusive));

        int years = Math.toIntExact(monthsUntil / MONTHS_IN_YEAR);
        int months = (int) (monthsUntil % MONTHS_IN_YEAR);
        int days = (int) this.plusMonths(monthsUntil).daysUntil(endDateExclusive);

        return DiscordianChronology.INSTANCE.period(years, months, days);
    }

    //-----------------------------------------------------------------------
    @Override
    public long toEpochDay() {
        long year = prolepticYear;
        long discordianEpochDay = ((year - OFFSET_FROM_ISO_0000 - 1) * 365) + getLeapYearsBefore(year) + (getDayOfYear() - 1);
        return discordianEpochDay - DISCORDIAN_1167_TO_ISO_1970;
    }

    //-------------------------------------------------------------------------
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder(30);
        buf.append(DiscordianChronology.INSTANCE.toString())
                .append(" ")
                .append(DiscordianEra.YOLD)
                .append(" ")
                .append(prolepticYear);
        if (month == 0) {
            buf.append(" St. Tib's Day");
        } else {
            buf.append("-").append(month)
                    .append(day < 10 ? "-0" : "-").append(day);
        }
        return buf.toString();
    }

}