AccountingYearDivision.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 java.time.DateTimeException;
import java.time.temporal.ChronoField;
import java.time.temporal.ValueRange;
import java.util.Arrays;

/**
 * How an Accounting year is divided.
 * <p>
 * An Accounting calendar system generally divides a year into smaller periods, similar in length to regular calendar months.
 * The most common divisions either use 12 such 'months' (requiring one every quarter to be 5 weeks instead of 4),
 * or use 13 of 4 weeks each (making one quarter have an extra month, or each quarter have partial months).
 *
 * <h3>Implementation Requirements:</h3>
 * This is an immutable and thread-safe enum.
 */
public enum AccountingYearDivision {

    /**
     * The singleton instance for a year divided into 4 quarters, 
     * each having 3 months with lengths of 4, 4, and 5 weeks, respectively.
     */
    QUARTERS_OF_PATTERN_4_4_5_WEEKS(new int[] {4, 4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5}),
    /**
     * The singleton instance for a year divided into 4 quarters, 
     * each having 3 months with lengths of 4, 5, and 4 weeks, respectively.
     */
    QUARTERS_OF_PATTERN_4_5_4_WEEKS(new int[] {4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4}),
    /**
     * The singleton instance for a year divided into 4 quarters, 
     * each having 3 months with lengths of 5, 4, and 4 weeks, respectively.
     */
    QUARTERS_OF_PATTERN_5_4_4_WEEKS(new int[] {5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4, 4}),
    /**
     * The singleton instance for a year divided into 13 even months,
     * each having 4 weeks.
     */
    THIRTEEN_EVEN_MONTHS_OF_4_WEEKS(new int[] {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4});

    /**
     * The number of weeks in each month.
     */
    private final int[] weeksInMonths;
    /**
     * The range of months in each year.
     */
    private final ValueRange monthsInYearRange;
    /**
     * The elapsed number of weeks at the start of each month.
     */
    private final int[] elapsedWeeks;

    //-----------------------------------------------------------------------
    /**
     * Private constructor for enum, for creating cached info.
     * 
     * @param weeksInMonths The number of weeks in each month (period).
     */
    private AccountingYearDivision(int[] weeksInMonths) {
        this.weeksInMonths = weeksInMonths;

        this.monthsInYearRange = ValueRange.of(1, weeksInMonths.length);

        this.elapsedWeeks = new int[weeksInMonths.length];
        for (int i = 1; i < weeksInMonths.length; i++) {
            elapsedWeeks[i] = elapsedWeeks[i - 1] + weeksInMonths[i - 1];
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the range of months in a year.
     * <p>
     * <ul>
     * <li>The AccountingYearDivision {@code QUARTERS_OF_PATTERN_4_4_5_WEEKS} range is [1, 12].
     * <li>The AccountingYearDivision {@code QUARTERS_OF_PATTERN_4_5_4_WEEKS} range is [1, 12].
     * <li>The AccountingYearDivision {@code QUARTERS_OF_PATTERN_5_4_4_WEEKS} range is [1, 12].
     * <li>The AccountingYearDivision {@code THIRTEEN_EVEN_MONTHS_OF_4_WEEKS} range is [1, 13].
     * </ul>
     *
     * @return the range of months (periods) in a year.
     */
    ValueRange getMonthsInYearRange() {
        return monthsInYearRange;
    }

    /**
     * Gets the length of the year in months.
     * 
     * @return The length of the year in months.
     */
    int lengthOfYearInMonths() {
        return weeksInMonths.length;
    }

    //-----------------------------------------------------------------------
    /**
     * Get the number of weeks in the given month (period). 
     * 
     * @param month  The month for which to get the count of weeks.
     * @return The count of weeks in the given month.
     * @throws DateTimeException if the month isn't in the valid range of months.
     */
    int getWeeksInMonth(int month) {
        return getWeeksInMonth(month, 0);
    }

    /**
     * Get the number of weeks in the given month (period), with the leap year in the indicated month.
     * 
     * @param month  The month for which to get the count of weeks.
     * @param leapWeekInMonth  The month in which the leap-week resides
     * @return The count of weeks in the given month, including any leap week.
     * @throws DateTimeException if the month isn't in the valid range of months.
     */
    int getWeeksInMonth(int month, int leapWeekInMonth) {
        month = monthsInYearRange.checkValidIntValue(month, ChronoField.MONTH_OF_YEAR);
        leapWeekInMonth = (leapWeekInMonth == 0 ? 0 : monthsInYearRange.checkValidIntValue(leapWeekInMonth, ChronoField.MONTH_OF_YEAR));
        return weeksInMonths[month - 1] + (month == leapWeekInMonth ? 1 : 0);
    }

    //-----------------------------------------------------------------------
    /**
     * Get the number of weeks elapsed before the start of the month.
     * 
     * @param month The month
     * @return The number of weeks elapsed before the start of the month.
     * @throws DateTimeException if the month isn't in the valid range of months.
     */
    int getWeeksAtStartOfMonth(int month) {
        return getWeeksAtStartOfMonth(month, 0);
    }

    /**
     * Get the number of weeks elapsed before the start of the month.
     * 
     * @param month The month
     * @param leapWeekInMonth  The month in which the leap-week resides
     * @return The number of weeks elapsed before the start of the month, including any leap week.
     * @throws DateTimeException if the month isn't in the valid range of months.
     */
    int getWeeksAtStartOfMonth(int month, int leapWeekInMonth) {
        month = monthsInYearRange.checkValidIntValue(month, ChronoField.MONTH_OF_YEAR);
        leapWeekInMonth = (leapWeekInMonth == 0 ? 0 : monthsInYearRange.checkValidIntValue(leapWeekInMonth, ChronoField.MONTH_OF_YEAR));
        return elapsedWeeks[month - 1] + (leapWeekInMonth != 0 && month > leapWeekInMonth ? 1 : 0);
    }

    /**
     * Get the month from a count of elapsed weeks.
     * 
     * @param weeksElapsed The weeks elapsed since the start of the year.
     * @return the month
     * @throws DateTimeException if the month isn't in the valid range of months, 
     *   or the week isn't in the valid range.
     */
    int getMonthFromElapsedWeeks(int weeksElapsed) {
        return getMonthFromElapsedWeeks(weeksElapsed, 0);
    }

    /**
     * Get the month from a count of elapsed weeks.
     * 
     * @param weeksElapsed The weeks elapsed since the start of the year.
     * @param leapWeekInMonth  The month in which the leap-week resides
     * @return the month
     * @throws DateTimeException if the month isn't in the valid range of months, 
     *   or the week isn't in the valid range.
     */
    int getMonthFromElapsedWeeks(int weeksElapsed, int leapWeekInMonth) {
        if (weeksElapsed < 0 || weeksElapsed >= (leapWeekInMonth == 0 ? 52 : 53)) {
            throw new DateTimeException("Count of '" + elapsedWeeks.length + "' elapsed weeks not valid,"
                    + " should be in the range [0, " + (leapWeekInMonth == 0 ? 52 : 53) + ")");
        }
        leapWeekInMonth = (leapWeekInMonth == 0 ? 0 : monthsInYearRange.checkValidIntValue(leapWeekInMonth, ChronoField.MONTH_OF_YEAR));

        int month = Arrays.binarySearch(elapsedWeeks, weeksElapsed);
        // Binary search returns 0-indexed if found, negative - 1 for insert position if not.
        month = (month >= 0 ? month + 1 : 0 - month - 1);
        // Need to move to previous month if there was a leap week and in the first week.
        return leapWeekInMonth == 0 || month <= leapWeekInMonth || weeksElapsed > elapsedWeeks[month - 1] ? month : month - 1;
    }
}