3

I am doing the following programming exercise: Unlucky Days. The statement is:

Friday 13th or Black Friday is considered as unlucky day. Calculate how many unlucky days are in the given year.

Find the number of Friday 13th in the given year.

Input: Year as an integer.

Output: Number of Black Fridays in the year as an integer.

Examples:

unluckyDays(2015) == 3 unluckyDays(1986) == 1

First I tried the following code:

import java.time.*;
import java.time.format.*;
import java.util.Locale;

public class Kata {
  public static int unluckyDays/**/(int year) {
    System.out.println("\nyear: "+year);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/M/dd");
    for(int i = 1; i <= 12; i++){
      LocalDate date = LocalDate.parse(String.format("%d/%d/13",year,i));
      DayOfWeek dow = date.getDayOfWeek();
      String output = dow.getDisplayName(TextStyle.FULL, Locale.US);
      System.out.println("\noutput: "+output);
    }
    return 0;
  }
}

Where we get the following exception:

java.time.format.DateTimeParseException: Text '2015/1/13' could not be parsed at index 4
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948)
    at java.base/java.time.LocalDate.parse(LocalDate.java:428)
    at java.base/java.time.LocalDate.parse(LocalDate.java:413)
    at Kata.unluckyDays(Kata.java:10)

So as we see, the '/' at index 4 in the string used for LocalDate.parse(String.format("%d/%d/13",year,i)); is wrong.

Then I decided to change the format to use '-' instead of '/'

import java.time.*;
import java.time.format.*;
import java.util.Locale;

public class Kata {
  public static int unluckyDays/**/(int year) {
    System.out.println("\nyear: "+year);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-dd");
    for(int i = 1; i <= 12; i++){
      LocalDate date = LocalDate.parse(String.format("%d-%d-13",year,i));
      DayOfWeek dow = date.getDayOfWeek();
      String output = dow.getDisplayName(TextStyle.FULL, Locale.US);
      System.out.println("\noutput: "+output);
    }
    return 0;
  }
}

And with the previous code, the exception being thrown is:

java.time.format.DateTimeParseException: Text '2015-1-13' could not be parsed at index 5
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948)
    at java.base/java.time.LocalDate.parse(LocalDate.java:428)
    at java.base/java.time.LocalDate.parse(LocalDate.java:413)
    at Kata.unluckyDays(Kata.java:10)

So as we see there is a difficulty with the one digit month '1'.

To continue I put the condition to pad with a 0 those months which only have one digit:

import java.time.*;
import java.time.format.*;
import java.util.Locale;

public class Kata {
  public static int unluckyDays/**/(int year) {
    System.out.println("\nyear: "+year);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-dd");
    int count = 0;
    for(int i = 1; i <= 12; i++){
      LocalDate date = LocalDate.parse(String.format("%d-%s-13",year,String.valueOf(i).length() < 2 ? "0"+i : i));
      DayOfWeek dow = date.getDayOfWeek();
      String output = dow.getDisplayName(TextStyle.FULL, Locale.US);
      System.out.println("\noutput: "+output);
      if(output.equals("Friday")){
        count++;  
      }
    }
    return count;
  }
}

With this condition it works. However how could we indicate in the DateTimeFormatter.ofPattern("yyyy-M-dd"); that months can have only one digit.

In addition, what should we do to use a different date format, for example: DateTimeFormatter.ofPattern("dd-M-yyyy");

Because of I have tried:

import java.time.*;
import java.time.format.*;
import java.util.Locale;

public class Kata {
  public static int unluckyDays/**/(int year) {
    System.out.println("\nyear: "+year);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-M-yyyy");
    int count = 0;
    for(int i = 1; i <= 12; i++){
      LocalDate date = LocalDate.parse(String.format("13-%s-%d",String.valueOf(i).length() < 2 ? "0"+i : i,year));
      DayOfWeek dow = date.getDayOfWeek();
      String output = dow.getDisplayName(TextStyle.FULL, Locale.US);
      System.out.println("\noutput: "+output);
      if(output.equals("Friday")){
        count++;  
      }
    }
    return count;
  }
}

Being the output quite interesting:

java.time.format.DateTimeParseException: Text '13-01-2015' could not be parsed at index 0
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948)
    at java.base/java.time.LocalDate.parse(LocalDate.java:428)
    at java.base/java.time.LocalDate.parse(LocalDate.java:413)
    at Kata.unluckyDays(Kata.java:11)

As we observe the '13' for days is causing an exception, at index 0, which I do not understand.

In addition I have read:

How could we know, why LocalDate parse throws exceptions‽

Yone
  • 2,064
  • 5
  • 25
  • 56
  • 1
    Impressive research effort. Like. – Ole V.V. Dec 28 '19 at 11:41
  • 1
    You could have avoided a lot of trouble by simply not using strings at all. [LocalDate.of(year, i, 13)](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/time/LocalDate.html#of%28int,int,int%29) works just fine. – VGR Dec 28 '19 at 16:45

4 Answers4

4

You are close from success. You have two options here:

  • Either use a DateTimeFormatter and pass it to your LocalDate.parse: LocalDate date = LocalDate.parse("13/01/2019", new DateTimeFormatter("dd/MM/yyyy");
    Check this website for more information: https://www.baeldung.com/java-string-to-date
  • Or, you can use the method LocalDate.of(year, month, day);: In your case, this will be LocalDate date = LocalDate.of(year, i, 13);. If you check the javadoc, you will see that for the month parameter, 1 is January and 12 is December. Basically, you are good to go with that. And you don't need a DateTimeFormatter or a String.format...

I've tried your code, both way works.

Edit:
I'd like to answer your last question. Every time you call LocalDate.parse("anyStringRepresentingADate"), what happens under the hood is that LocalDate is using a default DateTimeFormatter:

public static LocalDate parse(CharSequence text) {
    return parse(text, DateTimeFormatter.ISO_LOCAL_DATE);
}

And DateTimeFormatter.ISO_LOCAL_DATE is 'yyyy-MM-dd'.
When you do LocalDate.parse("13/1/2020"), your string does not match the default format, hence the exception.

RUARO Thibault
  • 2,672
  • 1
  • 9
  • 14
  • 2
    I’d go for `LocalDate.of(year, i, 13)` with no hesitation. There’s no need to format strings and parse them back for your exercise. Or even more elegantly: `for (m : Month.values())` and then `LocalDate.of(year, m, 13)`, using the overloaded `of` method that accepts an instance of the `Month` enum as second argument. – Ole V.V. Dec 28 '19 at 11:39
1

you must pass formatter as the second parameter

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-M-yyyy");
LocalDate date = LocalDate.parse("01-1-2019", formatter);
1

Stream and lambda

Other Answers are correct. For fun, here are a couple approaches using lambda syntax, streams, and predicate.

Stream of months

From the comment by Ole V.V., this solution uses a stream of Month objects taken from that enum class via the Arrays utility class, January-December. For each month we can ask if the 13th of the month for a particular year is a Friday.

We are instantiating a LocalDate on the fly, a date-only value without a time-of-day and without a time zone.

int year = 2020 ;
long count = 
        Arrays                            // Utility class, `java.util.Arrays`.
        .stream(                          // Generate a stream from an array.
            Month.values()                // Generate an array from all the objects defined on this enum `Month`: An object for January, and February, and so on.
        )                                 // Returns a `Stream` object.
        .filter(                          // Applies a `Predicate` to test each object produced by the stream. Those that pass the test are fed into a new second stream.
            ( Month month ) ->            // For each `Month` enum object.
            LocalDate                     // Represent a date-only value, a year-month-day.
            .of( year , month , 13 )      // Instantiate a `LocalDate` from inputs for year number, `Month` enum object, and day number.
            .getDayOfWeek()               // Interrogate for the `DayOfWeek` enum object that represents the day-of-week for that particular date.
            .equals( DayOfWeek.FRIDAY )   // Ask if the day-of-week for this date is a Friday.
        )                                 // Returns a new 2nd `Stream` object.
        .count()                          // Returns the number of Friday-the-13th dates are being produced by this second stream. The year 2020 has two such dates.
;

See this code run live at IdeOne.com.

Stream of dates

In this other solution, we get a Stream of LocalDate objects, for each date between the start and stop. Notice that the date range is Half-Open, where the beginning in inclusive while the ending is exclusive. So a year starts on the 1st of the year and runs up to, but does not include, the 1st of the following year.

For each date in the stream, we test using a Predicate checking if (a) the day-of-month is thirteen, and (b) the DayOfWeek is Friday. All the LocalDate objects passing the test are put into a newly generated List.

int input = 2020;
Year year = Year.of( input );
LocalDate start = year.atDay( 1 );
LocalDate stop = year.plusYears( 1 ).atDay( 1 );

List < LocalDate > fridayThe13ths =
        start
                .datesUntil( stop )
                .filter(
                        ( LocalDate localDate ) ->
                                ( localDate.getDayOfMonth() == 13 )
                                        &&
                                        localDate.getDayOfWeek().equals( DayOfWeek.FRIDAY )
                )
                .collect( Collectors.toList() )
;

Dump to console. Generate text in standard ISO 8601 format.

System.out.println( "fridayThe13ths = " + fridayThe13ths );

fridayThe13ths = [2020-03-13, 2020-11-13]

To verify that these results are indeed Friday-the-13th, let's generate strings representing their value. We define a formatter for automatically localized output that includes the day-of-week.

DateTimeFormatter f = DateTimeFormatter.ofLocalizedDate( FormatStyle.FULL ).withLocale( Locale.US );
fridayThe13ths.forEach( ( LocalDate localDate ) -> System.out.println( localDate.format( f ) ) );

Friday, March 13, 2020

Friday, November 13, 2020

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • That’s fun alright. My version would be `long count = Arrays.stream(Month.values()).filter(m -> LocalDate.of(input, m, 13).getDayOfWeek().equals(DayOfWeek.FRIDAY)).count();`. – Ole V.V. Dec 29 '19 at 06:52
  • @OleV.V. Thanks. I added a section showing your code. – Basil Bourque Dec 30 '19 at 03:18
0

Here a method which calculate black fridays:

private static void unluckyDays(int year) {
    int blackFridays = 0;
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/M/dd");
    for (int i = 1; i <= 12; i++) {
        LocalDate date = LocalDate.parse(String.format("%d/%d/13", year, i), formatter);
        DayOfWeek dow = date.getDayOfWeek();
        if (dow.getValue() == 5) {
            blackFridays++;
        }
    }
    System.out.println(String.format("Year %s has %s black fridays", year, blackFridays));
}
user8352
  • 1
  • 1
  • Thanks for your contribution. It’s correct. I think that the question was rather about why the code in the question always threw an exception than about having us solve the exercise for the questioner. Also we don’t learn much from the code alone; some explanation of how it works would be nice. PS Please avoid magic numbers as `5`; use `dow.equals(DayOfWeek.FRIDAY)` or `dow == DayOfWeek.FRIDAY`. – Ole V.V. Dec 28 '19 at 11:48