3

All the examples I've seen of calendar view libraries and calendar based date pickers, use the old Calendar API. However, I have not found any that use the Java 8 date API to build out a calendar view. This is something I'm trying to achieve and have essentially done it but the issue I'm running into is setting the first day of the week using the locale.

I've managed to get and display all the dates for each day of the week for a given month. However, the issue I'm having is that the resulting calendar does not start from the first day of the week based on my locale. The DayOfWeek enum which Java.Time uses, starts counting the days from Monday to Sunday (1 - 7). However I want the calendar to display the days by the locale, in my case, Sunday to Saturday.

According to the documentation, the WeekFields class provides access to the days of the week based on locale, with the "correct" values, but I'm not sure how to properly utilize it.

This is what I have done so far:

private enum class DaysOfWeek {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
}

fun TimetableCalendar() {
    val dates = getDaysOfMonth(LocalDate.now().year, LocalDate.now().month)
    
    // WORK AROUND: Hardcoding the first day of the week by manually inserting them into a new map
    val tempDates = hashMapOf<DaysOfWeek, MutableList<LocalDate>>()
    tempDates[DaysOfWeek.SUNDAY] = dates[DayOfWeek.SUNDAY] !!
    tempDates[DaysOfWeek.MONDAY] = dates[DayOfWeek.MONDAY] !!
    tempDates[DaysOfWeek.TUESDAY] = dates[DayOfWeek.TUESDAY] !!
    tempDates[DaysOfWeek.WEDNESDAY] = dates[DayOfWeek.WEDNESDAY] !!
    tempDates[DaysOfWeek.THURSDAY] = dates[DayOfWeek.THURSDAY] !!
    tempDates[DaysOfWeek.FRIDAY] = dates[DayOfWeek.FRIDAY] !!
    tempDates[DaysOfWeek.SATURDAY] = dates[DayOfWeek.SATURDAY] !!

    // Sort the days by ordinal, 0 - 6
    val sortedDates = tempDates.toSortedMap(compareBy {
        d -> d.ordinal
    })

    LazyVerticalGrid(
        cells = GridCells.Fixed(7),
        contentPadding = PaddingValues(16. dp)
    ) {
        // Display short day of week name
        items(sortedDates.keys.toList()) { dayOfWeek ->
                Text(text = dayOfWeek.name.substring(0, 3), textAlign = TextAlign.Center)
        }
        itemsIndexed(sortedDates.values.toList()) { _, date ->
            Column(
                modifier = Modifier.padding(4. dp)
            ) {
                date.forEach { day ->
                        DateView(date = day.dayOfMonth.toString())
                }
            }
        }
    }
}

/**
 * Returns the dates for each day of the week in the given [year] and [month]
 * Including dates of the previous and next month if the [month] does not
 * begin on the first or last day of the week
 */
fun getDaysOfMonth(year: Int, month: Month): HashMap<DayOfWeek, MutableList<LocalDate>> {
    val weekFields = WeekFields.of(Locale.getDefault())

    val daysOfWeek = mutableSetOf<DayOfWeek>()
    daysOfWeek.add(DayOfWeek.SUNDAY)
    daysOfWeek.add(DayOfWeek.MONDAY)
    daysOfWeek.add(DayOfWeek.TUESDAY)
    daysOfWeek.add(DayOfWeek.WEDNESDAY)
    daysOfWeek.add(DayOfWeek.THURSDAY)
    daysOfWeek.add(DayOfWeek.FRIDAY)
    daysOfWeek.add(DayOfWeek.SATURDAY)

    val ym = YearMonth.of(year, month)
    val firstOfMonth = ym.atDay(1) // first day of the month
    val lastDayOfMonth = ym.atDay(ym.lengthOfMonth())

    val dayDates = hashMapOf<DayOfWeek, MutableList<LocalDate>>()

    // Get all the dates for each day of the week
    daysOfWeek.forEach { day ->
            val dates = mutableListOf<LocalDate>()
        var ld = firstOfMonth.with(TemporalAdjusters.dayOfWeekInMonth(1, day))
        do {
            dates.add(ld)
            ld = ld.plusWeeks(1)
        } while (YearMonth.from(ld).equals(ym))
        dayDates[day] = dates
    }

    // If current month does not start on a Sunday, get the last few days of the previous month
    if (firstOfMonth.dayOfWeek != weekFields.firstDayOfWeek) {
        val previousMonth = YearMonth.of(year, month.minus(1))
        var lastDateOfPrevMonth = LocalDate.of(year, previousMonth.month, previousMonth.atEndOfMonth().dayOfMonth)

        do {
            dayDates[lastDateOfPrevMonth.dayOfWeek]?.add(0, lastDateOfPrevMonth)
            lastDateOfPrevMonth = lastDateOfPrevMonth.minusDays(1)
        } while (lastDateOfPrevMonth.dayOfWeek != DayOfWeek.SATURDAY)
    }

    // If current month does not end on a saturday, get the first few days of the next month
    if (lastDayOfMonth.dayOfWeek != weekFields.firstDayOfWeek.minus(1)) {
        val nextMonth = YearMonth.of(year, month.plus(1))
        var firstDateOfNextMonth = LocalDate.of(year, nextMonth.month, 1)

        do {
            dayDates[firstDateOfNextMonth.dayOfWeek]?.add(firstDateOfNextMonth)
            firstDateOfNextMonth = firstDateOfNextMonth.plusDays(1)
        } while (firstDateOfNextMonth.dayOfWeek != DayOfWeek.SUNDAY)
    }

    return dayDates
}

The above code works as intended and displays a calendar with the week starting on Sunday. Would it make sense to manually insert the days of the week into a new hashmap using a new enum which starts at Sunday, as I've done? Or would it make more sense to let the date api handle it (if possible) and how?

Or am I just taking the wrong approach and the Calendar API would be a much better option?

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
user15187712
  • 57
  • 1
  • 5
  • Never use the legacy date-time classes. They are supplanted entirely by the modern *java.time* classes defined in JSR 310. The legacy classes are terrible, deeply flawed in design. – Basil Bourque Sep 06 '21 at 02:06

4 Answers4

3

No need for your to define private enum class DaysOfWeek {…}. See DayOfWeek.

See How to get the first day of week for a certain Locale / Country.

DayOfWeek firstDayOfWeek = WeekFields.of( Locale.CANADA_FRENCH ).getFirstDayOfWeek();

Use YearMonth to represent, well, the year and month.

ZoneId z = ZoneId.of( "America/Montreal" ) ;
YearMonth yearMonth = YearMonth.now( z ) ;

Get the first day of that month.

LocalDate firstOfMonth = yearMonth.atDay( 1 ) ;

Move back in time, if need be, to find the first day of week. Use a TemporalAdjuster implementation.

TemporalAdjuster ta = TemporalAdjusters.previousOrSame( firstDayOfWeek ) ;
LocalDate startOfCalendar = firstOfMonth.with( ta );

Build our calendar. Use a navigable map to collect each week number to a list of the LocalDate objects.

NavigableMap< Integer , List< LocalDate > > calendar = new TreeMap<>() ;

Or just use a list of lists.

List< List < LocalDate > > calendar = new ArrayList<>() ;

Make a week.

LocalDate ld = startOfCalendar ; 
List< LocalDate > week = ld.datesUntil( ld.plusWeeks(1) ).toList() ;

If using an older version of Java, replace that .datesUntil line with your own loop.

int daysInWeek = 7;
List < LocalDate > week = new ArrayList <>( daysInWeek );
for ( int indexIntoWeek = 0 ; indexIntoWeek < daysInWeek ; indexIntoWeek++ )
{
    week.add( ld.plusDays( indexIntoWeek ) );
}

Add that week to your map, and continue until into a new YearMonth.

Pull all that code together.

// Populate data.
DayOfWeek firstDayOfWeek = WeekFields.of( Locale.CANADA_FRENCH ).getFirstDayOfWeek();

ZoneId z = ZoneId.of( "America/Montreal" );
YearMonth yearMonth = YearMonth.now( z );
LocalDate firstOfMonth = yearMonth.atDay( 1 );

TemporalAdjuster ta = TemporalAdjusters.previousOrSame( firstDayOfWeek );
LocalDate startOfCalendar = firstOfMonth.with( ta );

NavigableMap < Integer, List < LocalDate > > calendar = new TreeMap <>();
int nthWeek = 1;
LocalDate ld = startOfCalendar;
while ( ! YearMonth.from( ld ).isAfter( yearMonth ) )
{
    List < LocalDate > week = ld.datesUntil( ld.plusWeeks( 1 ) ).toList();
    calendar.put( nthWeek++ , week );
    ld = ld.plusWeeks( 1 );  // Set up the next loop. Increment `ld` to the next week.
}

// Report results.
for ( Integer weekNumber : calendar.keySet() )
{
    DateTimeFormatter f = DateTimeFormatter.ofLocalizedDate( FormatStyle.SHORT ).withLocale( Locale.US );
    System.out.println( "weekNumber = " + weekNumber );
    for ( LocalDate localDate : calendar.get( weekNumber ) )
    {
        String output = localDate.getDayOfWeek().getDisplayName( TextStyle.FULL , Locale.US ) + " = " + localDate.format( f );
        System.out.println( output );
    }
}

When run.

weekNumber = 1
Sunday = 8/29/21
Monday = 8/30/21
Tuesday = 8/31/21
Wednesday = 9/1/21
Thursday = 9/2/21
Friday = 9/3/21
Saturday = 9/4/21
weekNumber = 2
Sunday = 9/5/21
Monday = 9/6/21
Tuesday = 9/7/21
Wednesday = 9/8/21
Thursday = 9/9/21
Friday = 9/10/21
Saturday = 9/11/21
weekNumber = 3
Sunday = 9/12/21
Monday = 9/13/21
Tuesday = 9/14/21
Wednesday = 9/15/21
Thursday = 9/16/21
Friday = 9/17/21
Saturday = 9/18/21
weekNumber = 4
Sunday = 9/19/21
Monday = 9/20/21
Tuesday = 9/21/21
Wednesday = 9/22/21
Thursday = 9/23/21
Friday = 9/24/21
Saturday = 9/25/21
weekNumber = 5
Sunday = 9/26/21
Monday = 9/27/21
Tuesday = 9/28/21
Wednesday = 9/29/21
Thursday = 9/30/21
Friday = 10/1/21
Saturday = 10/2/21
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
3

WeekFields.dayOfWeek()

You are entirely correct: to sort the days of the week correctly for a locale, that is, with the day of week first that according to that locale is the first day of the week, you need to use an appropriate WeekFields object. The following Java method gives just a short demonstration:

public static void sortAndPrintDaysOfWeek(Locale loc) {
    List<DayOfWeek> daysOfTheWeek = Arrays.asList(DayOfWeek.values());
    WeekFields wf = WeekFields.of(loc);

    daysOfTheWeek.sort(Comparator.comparingInt(dow -> dow.get(wf.dayOfWeek())));

    System.out.println(daysOfTheWeek);
}

I trust you to apply a similar comparator to your Kotlin sorted map. Trying the above method out:

    sortAndPrintDaysOfWeek(Locale.FRANCE);

Output:

[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]

    sortAndPrintDaysOfWeek(Locale.US);

[SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]

    sortAndPrintDaysOfWeek(Locale.forLanguageTag("ar-EG"));

[SATURDAY, SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
1

It is quite easy with java.time, the modern Date-Time API. The first step is to find the first day of the week as per your Locale, which you can do as follows:

DayOfWeek firstDayOfWeek = WeekFields.of(locale).getFirstDayOfWeek();

If all you need is the list of weekdays:

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        // Let's test it
        System.out.println(getWeekDays(Locale.UK));
        System.out.println(getWeekDays(Locale.US));
    }

    static List<DayOfWeek> getWeekDays(Locale locale) {
        LocalDate localDate = LocalDate.now();

        // First day of week
        DayOfWeek firstDayOfWeek = WeekFields.of(locale).getFirstDayOfWeek();

        LocalDate date = localDate.with(TemporalAdjusters.dayOfWeekInMonth(0, firstDayOfWeek));
        return IntStream.rangeClosed(0, 6)
                    .mapToObj(i -> date.plusDays(i).getDayOfWeek())
                    .collect(Collectors.toList());
    }
}

Output:

[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
[SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]

ONLINE DEMO

In case you need to extend it to print the full calendar of the month:

The rest of the logic revolves around creating the labels with proper spacing, which anyone should be able to do with the basic knowledge of the loop.

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        // Let's test it
        System.out.println(getMonthCalendar(Locale.UK));
        System.out.println("-+-+-+-+-+-+-+-+-+-+-+-+-+-");
        System.out.println(getMonthCalendar(Locale.US));
    }

    static String getMonthCalendar(Locale locale) {
        LocalDate localDate = LocalDate.now();
        YearMonth ym = YearMonth.of(localDate.getYear(), localDate.getMonthValue());
        StringBuilder sb = new StringBuilder();

        // First day of week
        DayOfWeek firstDayOfWeek = WeekFields.of(locale).getFirstDayOfWeek();

        LocalDate date = localDate.with(TemporalAdjusters.dayOfWeekInMonth(0, firstDayOfWeek));
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("EEE", locale);
        sb.append(
                    IntStream.rangeClosed(0, 6)
                    .mapToObj(i -> dtf.format(date.plusDays(i)))
                    .collect(Collectors.joining(" "))
        )
        .append(System.lineSeparator());

        int counter = 1;

        // Print as many space as the difference between the day of week of 1st date of
        // the month and the first day of the week in that Locale
        int dayValue = localDate.withDayOfMonth(1).getDayOfWeek().getValue() - firstDayOfWeek.getValue();
        dayValue = dayValue < 0 ? 7 + dayValue : dayValue;
        for (int i = 0; i < dayValue; i++, counter++) {
            sb.append(String.format("%-4s", ""));
        }

        for (int i = 1; i <= ym.getMonth().length(ym.isLeapYear()); i++, counter++) {
            sb.append(String.format("%-4d", i));

            // Break the line if the value of the counter is multiple of 7
            if (counter % 7 == 0) {
                sb.append(System.lineSeparator());
            }
        }

        return sb.toString();
    }
}

Output:

Mon Tue Wed Thu Fri Sat Sun
        1   2   3   4   5   
6   7   8   9   10  11  12  
13  14  15  16  17  18  19  
20  21  22  23  24  25  26  
27  28  29  30  
-+-+-+-+-+-+-+-+-+-+-+-+-+-
Sun Mon Tue Wed Thu Fri Sat
            1   2   3   4   
5   6   7   8   9   10  11  
12  13  14  15  16  17  18  
19  20  21  22  23  24  25  
26  27  28  29  30  

ONLINE DEMO

Check the UK Calendar and the US Calendar for a comparision.

Learn more about the modern Date-Time API* from Trail: Date Time.


* For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7. If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
0

I think it's worth a try. Try using a combination of LocalTime and LocalDate in the API.

var dt = LocalDateTime.of(2021, 1, 1, 12, 45);// => 2021-01-01T12:45
It is possible to easily decompose into components:

var dt = LocalDateTime.of(2021, 1, 1, 12, 45);
// => 2021-01-01T12:45

var date = dt.toLocalDate();
// => 2021-01-01

var time = dt.toLocalTime();
// => 12:45

If the program detects errors, then in order to fix it, the Time API divides the responsibility into several classes.

  • ZoneOffset — offset from the time in UTC/GMT, from +14:00 to -12: 00.

  • ZoneRules — rules for changing the offset for a single time zone (for example, daylight saving time, historical changes). ZoneId is the identifier of the time zone, for example, Europe/Berlin. There are two different types of time zones available.

  • ZonedDateTime-binding to a specific ZoneId. OffsetDateTime / OffsetTime — date/time with an offset, but not tied to a specific time zone.