3
\$\begingroup\$

A friend of mine was practicing his programming skills with a textbook meant to prepare students for computer science exams. He asked for help with a specific task.

The task is to capture user input (day of week and a year in the range 1500 to 2005 inclusive), and output all instances of the weekday in February that year.

The differences between the Julian and Gregorian calendars are to be accounted for.

Now, most likely the idea behind this task was to have the student create an algorithm to manually calculate the dates. However, as Java SE seems to be allowed in exams in my country, I came up with the idea of utilizing the GregorianCalendar class (which, despite its name, combines the Julian and Gregorian calendars).

package calendar;

import java.text.DateFormat;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.Scanner;

public class CalendarTask {
    public static void main(String[] args) {
        GregorianCalendar cal = new GregorianCalendar();

        HashMap<String, Integer> daysOfWeek = new HashMap<>();
        daysOfWeek.put("monday", cal.MONDAY);
        daysOfWeek.put("tuesday", cal.TUESDAY);
        daysOfWeek.put("wednesday", cal.WEDNESDAY);
        daysOfWeek.put("thursday", cal.THURSDAY);
        daysOfWeek.put("friday", cal.FRIDAY);
        daysOfWeek.put("saturday", cal.SATURDAY);
        daysOfWeek.put("sunday", cal.SUNDAY);

        System.out.print("Enter day of week: ");
        Scanner sc = new Scanner(System.in);
        int dayOfWeek, year;
        try {
            dayOfWeek = daysOfWeek.get(sc.next().toLowerCase());
            System.out.print("Enter year (1500-2005 inclusive): ");
            year = Integer.parseInt(sc.next());
            if (year < 1500 || year > 2005) throw new Exception();

            System.out.println("Output:");
            DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.GERMANY);
            cal.set(cal.YEAR, year);
            trySetDay(cal, dayOfWeek, 1);
            do {
                System.out.println(df.format(cal.getTime()));
                cal.add(cal.DAY_OF_MONTH, 7);
            } while (cal.get(cal.MONTH) == cal.FEBRUARY);
        } catch (Exception e) {
            System.out.println("Incorrect input!");
        } finally {
            sc.close();
        }
    }

    private static void trySetDay(GregorianCalendar cal, int dayOfWeek, int weekOffset) {
        cal.set(cal.MONTH, cal.FEBRUARY);
        cal.set(cal.WEEK_OF_MONTH, weekOffset);
        cal.set(cal.DAY_OF_WEEK, dayOfWeek);
        if (cal.get(cal.MONTH) != cal.FEBRUARY) trySetDay(cal, dayOfWeek, weekOffset + 1);
    }
}

Example input and output (dates are output in DD.MM.YYYY format):

Enter day of week: Monday
Enter year (1500-2005 inclusive): 2000
Output:
07.02.2000
14.02.2000
21.02.2000
28.02.2000

The trySetDay() method is meant for cases when the attempted day belongs to the previous month. In such a case, the week offset is increased by one to make sure we're dealing with February.

This code works fine and I'm satisfied with it. The catch is there to handle NullPointerException (when attempting to assign null, a possible result of HashMap.get(), and also when parsing the year) and a generic Exception set to limit the possible input to the range 1500 to 2005.

What can be improved about this code? Is there anything that caught your eye instantly and that could be done better? Any and all feedback is appreciated.
Also, is it a good idea to access static class members via an instance of the class? Such as cal.MONDAY (where cal is an instance of GregorianCalendar), instead of GregorianCalendar.MONDAY?

\$\endgroup\$
  • \$\begingroup\$ Are you on Java 8? \$\endgroup\$ – h.j.k. Mar 13 '16 at 14:18
  • \$\begingroup\$ @h.j.k. Yes, I'm using Java SE 8. \$\endgroup\$ – rhino Mar 13 '16 at 14:21
  • \$\begingroup\$ BTW, for anyone interested, I believe the mathematical approach will be the Zeller's Congruence (e.g. see @rolfl's question). \$\endgroup\$ – h.j.k. Mar 14 '16 at 9:00
3
\$\begingroup\$

Java 8 Time APIs

Instead of the 'legacy' Calendar and DateFormat classes, you can rely on Java 8's new java.time.* APIs for more fluent chronology-related calculations.

For starters, your manually constructed Map can be replaced with a simple look-up on the DayOfWeek enum:

// scanner will be a wrapper over System.in
private static DayOfWeek getDayOfWeek(Scanner scanner) {
    String values = Arrays.toString(DayOfWeek.values());
    System.out.printf("Enter a day of week:%n%s%n", values);
    while (true) {
        try {
            return DayOfWeek.valueOf(scanner.nextLine().trim().toUpperCase());
        } catch (IllegalArgumentException e) {
            System.err.printf("Please try again with one of these:%n%s%n",
                    values);
        }
    }
}

The looping-validation ensures that only a valid DayOfWeek value is returned.

Next, you can use a couple of TemporalAdjusters to get the LocalDates you require:

Then, with a helpful serving of DateTimeFormatter, the main processing logic can just be:

try (Scanner scanner = new Scanner(System.in)) {
    DayOfWeek dayOfWeek = getDayOfWeek(scanner);
    // getYear(Scanner) returns an int between the year range, MONTH = 2
    LocalDate first = LocalDate.of(getYear(scanner), MONTH, 1)
                                .with(TemporalAdjusters.firstInMonth(dayOfWeek));
    LocalDate last = first.with(TemporalAdjusters.lastDayOfMonth());
    DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
                                                    .withLocale(Locale.GERMANY);
    for (LocalDate date = first; !date.isAfter(last);
            date = date.with(TemporalAdjusters.next(dayOfWeek))) {
        System.out.println(formatter.format(date));
    }
}

Other observations

  • It's not recommended to throw a generic Exception as it's... too vague. You can consider using IllegalArgumentException for invalid year inputs outside \$[1500, 2005]\$, as the exception type is then more defined.

  • More importantly, you shouldn't be throwing your own Exceptions and then catching it purely as a form of flow control.

  • HashMap<String, Integer> daysOfWeek = new HashMap<>() can be better written as Map<String, Integer> daysOfWeek = new HashMap<>(), to program to an interface rather than the implementation.

  • As illustrated above, you should also consider using try-with-resources on the Scanner instance for safe and efficient handling of the underlying I/O resource.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the helpful answer! But how would you address my last question (accessing static members via instances)? \$\endgroup\$ – rhino Mar 18 '16 at 15:13
  • \$\begingroup\$ @rhio wells I think that's somewhat unconventional. IDEs by default prompt you to switch to non-instance access, and if you're looking to (desparately) save some characters within a line, you can also do a import static. \$\endgroup\$ – h.j.k. Mar 18 '16 at 15:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.