Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get Retail (4-5-4) Calendar Week of Month from a date

Tags:

java

calendar

I need to get the 4-5-4 Calendar Week from a Date. Is there any utility like Georgian Calendar in Java for 4-5-4 Retail Calendar? If not, how can I create one? What all logic is needed? What is 53rd Week in case of Leap Year?

For example, if I pass a date (DD-MM-YYY) 04-03-2018 as input I should get March Week 1 as output. Or, if I give 01-04-2018 as input I should get March Week 5 as output.

Please help me by providing a way to build this utility.

like image 417
Sahil Chhabra Avatar asked Mar 19 '18 17:03

Sahil Chhabra


2 Answers

The following class should do it:

public class NrfMonthWeek {

    public static NrfMonthWeek getWeek(LocalDate date) {
        // Determine NRF calendar year.
        // The year begins on the Sunday in the interval Jan 29 through Feb 4.
        LocalDate firstDayOfNrfYear = date.with(MonthDay.of(Month.JANUARY, 29))
                .with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
        if (date.isBefore(firstDayOfNrfYear)) { // previous NRF year
            firstDayOfNrfYear = date.minusYears(1)
                    .with(MonthDay.of(Month.JANUARY, 29))
                    .with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
        }
        // 1-based week of NRF year
        int weekOfNrfYear = (int) ChronoUnit.WEEKS.between(firstDayOfNrfYear, date) + 1;
        assert 1 <= weekOfNrfYear && weekOfNrfYear <= 53 : weekOfNrfYear;
        YearMonth firstMonthOfNrfYear = YearMonth.from(firstDayOfNrfYear)
                .with(Month.FEBRUARY);
        if (weekOfNrfYear == 53) {
            // Special case: the last week of a 53 weeks year belongs to
            // the last month, January; this makes it a 5 weeks month.
            return new NrfMonthWeek(firstMonthOfNrfYear.plusMonths(11), 5);
        } else {
            // 1-based month of NRF year (1 = February through 12 = January).
            // A little math trickery to make the 4-5-4 pattern real.
            int monthOfNrfYear = (weekOfNrfYear * 3 + 11) / 13;
            // Number of weeks before the NRF month: 0 for February, 4 for March, 9 for April, etc.
            int weeksBeforeMonth = (monthOfNrfYear * 13 - 12) / 3;
            int weekOfMonth = weekOfNrfYear - weeksBeforeMonth;
            return new NrfMonthWeek(
                    firstMonthOfNrfYear.plusMonths(monthOfNrfYear - 1), weekOfMonth);
        }
    }

    private YearMonth month;
    /** 1 through 5 */
    private int weekOfMonth;

    public NrfMonthWeek(YearMonth month, int weekOfMonth) {
        this.month = Objects.requireNonNull(month);
        if (weekOfMonth < 1 || weekOfMonth > 5) {
            throw new IllegalArgumentException("Incorrect week number " + weekOfMonth);
        }
        this.weekOfMonth = weekOfMonth;
    }

    @Override
    public String toString() {
        return month.getMonth().getDisplayName(TextStyle.FULL, Locale.US)
                + " Week " + weekOfMonth;
    }
}

Let’s try it. Here I pass the two dates from your question to the getWeek method:

    System.out.println(NrfMonthWeek.getWeek(LocalDate.of(2018, Month.MARCH, 4)));
    System.out.println(NrfMonthWeek.getWeek(LocalDate.of(2018, Month.APRIL, 1)));

This prints the desired:

March Week 1
March Week 5

Though only month and week are printed, also the year is contained in the object returned from getWeek.

The formulas for calculating the month and week-of-month are cryptic. I have no really good argument why they work, though such an argument could probably be constructed. I have tested them with all relevant values, and you are free to do the same. Other than that, using java.time, the modern Java date and time API, it wasn’t too bad.

If that were me, I would have finer validation in the NrfMonthWeek constructor, only allowing week 5 in the months that may have 5 weeks. I am leaving that to you. And I would have a pretty thorough unit test.

Please check whether my understanding agrees with yours: If I have understood correctly from the example calendars that Basil Bourque linked to in his answer, the NRF 4-5-4 year starts with February. Its weeks begin on Sunday, and the first week of the year is the first week that contains at least 4 days of February. In other words, the week that contains February 4. In yet other words, the week that begins on a Sunday in the interval January 29 through February 4. Months March, June, September and December always have 5 weeks. In case of a 53 weeks year also January has 5 weeks.

like image 74
Ole V.V. Avatar answered Oct 14 '22 15:10

Ole V.V.


No support built-in

Neither the modern java.time classes nor the legacy date-time classes (Date/Calendar) directly support the National Retail Federation 4-5-4 Calendar.

Implement Chronology

I suspect the best way to solve this problem is to implement a Chronology for the java.time framework.

Java 8 and later bundle five implementations (HijrahChronology, IsoChronology, JapaneseChronology, MinguoChronology, ThaiBuddhistChronology). Their source is available in the OpenJDK project.

The ThreeTen-Extra project provides ten more chronologies (AccountingChronology, BritishCutoverChronology, CopticChronology, DiscordianChronology, EthiopicChronology, InternationalFixedChronology, JulianChronology, PaxChronology, Symmetry010Chronology, Symmetry454Chronology) whose source code might help.

like image 32
Basil Bourque Avatar answered Oct 14 '22 13:10

Basil Bourque