WeekFields to the rescue!

Java Development

Uzi Landsmann

Systemutvecklare

Java’s endless number of ways of calculating week numbers

Living in Sweden, week numbers are everywhere: in your vacation plans, in your kids’ school exam calendar, and in every work planning meeting. Working as a developer in an American AdTech company I seldom have to think about week numbers — until one day they suddenly asked me to calculate the number of impressions to expect every week and to include the week number in that calculation. Easy enough, I thought, Java would know the week number, right? Right?

Enter the complex world of calendars and week number calculation.


The ISO way

My first attempt was to assume that the Swedish way was the correct way. Sweden is, as we all know, the center of the world, and everybody knows we do things the correct way and adjust themselves to our style, right? So a bit of combined mental and web googling resulted in these two following facts:


1. Week #1 is the first week to contain 4 days.

2. The first day of the week is Monday.


Merging these two facts, it is easy enough to calculate every week number given a date. A bit of stack-overflowing gave the following code:

final Strategy theISOWay = date ->
    date.get(WeekFields.ISO.weekOfYear());

 

I didn’t know what WeekFields was at the time, but I liked that ISO thingy. It looked like the correct way. It looked like the standard way. The Swedish way. Looking at the definition of the Weekfields.ISO field reassured me:

The ISO-8601 definition, where a week starts on Monday and the first week has a minimum of 4 days.

Satisfied with this result, I quickly included the calculation in my code and sent a request to the receiving endpoint, and got the following response:

422: The week falls outside the start/end date.


The aligned way

Funny, I thought. That must mean that the people who wrote the receiving requests are rebels who insist on making things their own way and don’t see the world through Swedish eyes. I decided to see if Java has some other cards up its sleeve and found the ChronoField.ALIGNED_WEEK_OF_YEAR field:

ALIGNED_WEEK_OF_YEAR

The aligned week within a year.

This represents concept of the count of weeks within the period of a year where the weeks are aligned to the start of the year. This field is typically used with ALIGNED_DAY_OF_WEEK_IN_YEAR.

For example, in a calendar systems with a seven day week, the first aligned-week-of-year starts on day-of-year 1, the second aligned-week starts on day-of-year 8, and so on. Thus, day-of-year values 1 to 7 are in aligned-week 1, while day-of-year values 8 to 14 are in aligned-week 2, and so on.

This must be the way the renegade developers implemented their code, I decided, and adjusted my code as follows:

final Strategy theAlignedWay = date -> 
    date.get(ChronoField.ALIGNED_WEEK_OF_YEAR);

 

Once again, I sent a request to the receiving endpoint, and got the following response:

422: The week falls outside the start/end date.

 

The broadcast way

Frustrated, I was beginning to realize that as much as I enjoyed development through trial-and-error, perhaps it was time to discuss matters with whoever ordered the code. Confronted, they admitted to using a thing called The Broadcast Calendar — of which I have never heard before. Wikipedia has this to say about this calendar:

 

The broadcast calendar is a standardized calendar used primarily for the planning and purchase of radio and television programs and advertising. Every week in the broadcast calendar starts on a Monday and ends on a Sunday, and every month has either four or five such weeks. Broadcast calendar months thus have either 28 or 35 days.

 

The key link between the broadcast and Gregorian calendars is that the first week of every broadcast month always contains the Gregorian calendar first of the month. For example, if January 1 falls on a Saturday, then the broadcast calendar year would begin on the preceding Monday, December 27. Broadcast January would then have five weeks, ending on January 30, and the four weeks of broadcast February would begin on January 31. The number of weeks in a broadcast month is based on the number of Sundays that fall in that month with the period ending on the last Sunday of the month.

So the Broadcast Calendar was a mixture of the ISO calendar (weeks begin on Mondays) and the aligned calendar (week #1 begins on January 1). How do I implement this?

 

Turns out a lot of people had the same question, and the good people of Stack Overflow usually pointed toward one solution: use the WeekField class. I realized it was time for some RTFM.

 

Fortunately, I didn’t have to spend too much time reading the documentation as I found what I was looking for quite fast: the WeekFields.of() method, which has the following signature:

public static WeekFields of(
    DayOfWeek firstDayOfWeek, int minimalDaysInFirstWeek)


Sounds good, doesn’t it? I can define my own calendar that has Mondays as the first day of the week and has a minimum of one day. Easy. Now how to use it? Turns out it the WeekFields.weekOfYear() function returns a TemporalField.

Now that made me remember something. I’ve always found it strange that the Instant.get(TemporalField) method takes a TemporalField as argument, because when you try to use the TemporalFieldinterface in your code and add a dot to get the autocomplete feature to help you find the correct constant, well, you’re out of luck.

WeekFields 1

No help coming from the TemporalField interface


In order to get the needed fields, you could instead use the ChronoField interface, which extends the TemporalFieldinterface. Why didn’t they design Instant.get() with ChronoField as an argument instead?

WeekFields 2

A lot of helpful constants in the ChronoField interface

 

Now that I realized that WeekFields.weekOfYear() method also returns a TemporalField and that I could use it in my solution, it suddenly made some sense, and I could see that the designers of thejava.time package might not have been as evil as I assumed them to be. I could now use the following lines in my code:

final var broadcastCalendar = WeekFields.of(DayOfWeek.MONDAY, 1);
final Strategy theBroadcastWay = date ->
    date.get(broadcastCalendar.weekOfYear());

 

Using the new code to create a request I finally got what I wanted:

200: OK


Bonus: the good old way

I’ve been programming Java for a long time, and I was wondering how the good old GregorainCalendar would deal with the same problem. Turns out it could be achieved quite easily:

final Strategy theGoodOldWay = date -> GregorianCalendar
        .from(date.atStartOfDay(ZoneId.systemDefault()))
        .get(Calendar.WEEK_OF_YEAR);


Browsing through old Greg’s documentation I could, after some effort, see that it also uses the same Swedish way of counting weeks, with a minimum of 4 days for the first week and Mondays as the first day of the week.

The code

You might have been wondering what that Strategy thing in my code examples was all about. I realized early enough that my code should look quite the same regardless of which week number strategy I would use and therefore chose the strategy pattern when implementing it. I thought I’d put all my example code here for your (and for the future me’s) benefit. The idea here is to produce a list with unique week numbers between two dates and to print them along with the different strategies I use:

package calendarish;

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.time.temporal.WeekFields;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.stream.Stream;

public class Calendarish {

    interface Strategy {
        int getWeek(final LocalDate date);
    }

    private List<Integer> weeksBetween(
            final LocalDate startDate,
            final LocalDate endDate,
            final Strategy strategy) {

        final long durationInDays = Duration.between(
                startDate.atStartOfDay(),
                endDate.atStartOfDay()).toDays();

        return Stream.iterate(startDate, date -> date.plusDays(1))
                .limit(durationInDays)
                .map(strategy::getWeek)
                .distinct()
                .toList();
    }

    public static void main(String[] args) {
        final var startDate = LocalDate.of(2022, Month.FEBRUARY, 1);
        final var endDate = LocalDate.of(2022, Month.FEBRUARY, 13);

        final Strategy theGoodOldWay = date -> GregorianCalendar
                .from(date.atStartOfDay(ZoneId.systemDefault()))
                .get(Calendar.WEEK_OF_YEAR);

        final Strategy theISOWay = date -> date.get(WeekFields.ISO.weekOfYear());

        final Strategy theAlignedWay = date ->
                date.get(ChronoField.ALIGNED_WEEK_OF_YEAR);

        final var broadcastCalendar = WeekFields.of(DayOfWeek.MONDAY, 1);
        final Strategy theBroadcastWay = date ->
                date.get(broadcastCalendar.weekOfYear());

        final var calendarish = new Calendarish();
        System.out.println("The good old way: "
                + calendarish.weeksBetween(startDate, endDate, theGoodOldWay));
        System.out.println("The ISO way: "
                + calendarish.weeksBetween(startDate, endDate, theISOWay));
        System.out.println("The aligned way: "
                + calendarish.weeksBetween(startDate, endDate, theAlignedWay));
        System.out.println("The broadcast way: "
                + calendarish.weeksBetween(startDate, endDate, theBroadcastWay));
    }
}


Running the example code, I get the following results:

The good old way: [5, 6]
The ISO way: [5, 6]
The aligned way: [5, 6, 7]
The broadcast way: [6, 7]

 

Thank you for reading. Now go count some weeks!

Fler inspirerande inlägg du inte får missa