-1

Using Java 8

Goal

From two dates (for example: firstDay 2018-09-01 and lastDay 2018-11-10), I would like to create two arrays of firstDay and lastDay created by month. For example:

List<LocalDate> firstDays = [2018-09-01,2018-10-01,2018-11-01]
List<LocalDate> lastDays = [2018-09-30, 2018-10-31,2018-11-10]

Eventually, I would like this method to apply also for years (for example: firstDay 2018-12-10 and lastDay 2019-01-06).

Issue

I don't now what to use to fullfill that goal. I'm still searching. Could you help me please?

Youcef LAIDANI
  • 55,661
  • 15
  • 90
  • 140

3 Answers3

3

You can use plusMonth like so :

LocalDate firstDay = LocalDate.parse("2018-09-01");
LocalDate lastDay = LocalDate.parse("2018-11-10");

Long timeBetween = ChronoUnit.MONTHS.between(firstDay, lastDay);

// First day of firstDays is not changeable so add it like it is
List<LocalDate> firstDays = new ArrayList<>(Arrays.asList(firstDay));

// Generate the dates between the start and end date
firstDays.addAll(LongStream.rangeClosed(1, timeBetween)
        .mapToObj(f -> firstDay.withDayOfMonth(1).plusMonths(f))
        .collect(Collectors.toList()));

// For the lastDays, generate the dates
List<LocalDate> lastDays = LongStream.range(0, timeBetween)
        .mapToObj(f -> {
            LocalDate newDate = firstDay.plusMonths(f);
            return newDate.withDayOfMonth(newDate.lengthOfMonth());
        }).collect(Collectors.toList());
// Last day of lastDays is not changeable so add it like it is
lastDays.add(lastDay);

Output

[2018-09-01, 2018-10-01, 2018-11-01]
[2018-09-30, 2018-10-31, 2018-11-10]
Youcef LAIDANI
  • 55,661
  • 15
  • 90
  • 140
  • 1
    Nice use of streams, but I think `lastDays` is wrong for the last element: it should be `2018-11-10` and not `2018-11-30` – Alex M Oct 25 '18 at 09:34
  • Thank you @AlexM oops I don't see this, I will fix it, but I think it is a typo? – Youcef LAIDANI Oct 25 '18 at 09:59
  • 1
    I think the user expects to see `firstDay` instead of the 1st of the first month, and `lastDay` instead of the last day of the last month. It was not made clear in the question but kinda makes sense. – Alex M Oct 25 '18 at 10:01
  • Yes Alex M and YCF_L. I'm trying both versions. Yours and the iterative one below. I tested first the iterative one, because the first firstDay and the last lastDay correspond to the real two dates entered. Thanks a lot for your help. I need to do some code on top for my purpose. I tell you if it works. – Manuela CodingPadawan Oct 25 '18 at 10:08
  • @ManuelaCodingPadawan check my update I was wrong, thank you AlexM too – Youcef LAIDANI Oct 25 '18 at 10:24
  • 1
    YCF_L thanks it works. I use the one of AlexM because I understand it better. But both are working. Thanks to you and to @AlexM. – Manuela CodingPadawan Oct 25 '18 at 10:47
2

In an iterative style, and handling edge cases:

LocalDate startDate = LocalDate.of(2018, 9, 1);
LocalDate endDate = LocalDate.of(2018, 11, 10);

List<LocalDate> firstDays = new ArrayList<>();
List<LocalDate> lastDays = new ArrayList<>();

LocalDate firstOfMonth = startDate.withDayOfMonth(1);
LocalDate lastOfMonth = startDate.withDayOfMonth(startDate.lengthOfMonth());

while (firstOfMonth.isBefore(endDate)) {
    firstDays.add(firstOfMonth.isBefore(startDate) ? startDate : firstOfMonth);
    lastDays.add(endDate.isBefore(lastOfMonth) ? endDate : lastOfMonth);

    firstOfMonth = firstOfMonth.plus(1, ChronoUnit.MONTHS);
    lastOfMonth = firstOfMonth.withDayOfMonth(firstOfMonth.lengthOfMonth());
}

System.out.println(firstDays);
System.out.println(lastDays);

Output:

[2018-09-01, 2018-10-01, 2018-11-01]
[2018-09-30, 2018-10-31, 2018-11-10]
Alex M
  • 885
  • 9
  • 12
0

There are a couple of good answers already. Allow me to see if I can still devise something elegant.

Create just one list of objects

First, I suggest that you don’t want two lists. That’s an anti-pattern described here: Anti-pattern: parallel collections. Since each first day belongs to a corresponding last day from the other list, everything will be more convenient and less error-prone when you keep them together. For this purpose you may use some library class or devise your own:

public class DateInterval {

    LocalDate firstDay;
    LocalDate lastDay;

    public DateInterval(LocalDate firstDay, LocalDate lastDay) {
        if (lastDay.isBefore(firstDay)) {
            throw new IllegalArgumentException("Dates in wrong order");
        }
        this.firstDay = firstDay;
        this.lastDay = lastDay;
    }

    // getters and other stuff

    @Override
    public String toString() {
        return "" + firstDay + " - " + lastDay;
    }

}

With this class we need just one list:

    LocalDate firstDayOverall = LocalDate.of(2018, Month.DECEMBER, 10);
    LocalDate lastDayOverall = LocalDate.of(2019, Month.JANUARY, 6);

    if (lastDayOverall.isBefore(firstDayOverall)) {
        throw new IllegalStateException("Overall dates in wrong order");
    }
    LocalDate firstDayOfLastMonth = lastDayOverall.withDayOfMonth(1);
    LocalDate currentFirstDay = firstDayOverall;
    List<DateInterval> intervals = new ArrayList<>();
    while (currentFirstDay.isBefore(firstDayOfLastMonth)) {
        intervals.add(new DateInterval(currentFirstDay, currentFirstDay.with(TemporalAdjusters.lastDayOfMonth())));
        currentFirstDay = currentFirstDay.withDayOfMonth(1).plusMonths(1);
    }
    intervals.add(new DateInterval(currentFirstDay, lastDayOverall));

    System.out.println("Intervals: " + intervals);

The output from the above code snippet is:

Intervals: [2018-12-10 - 2018-12-31, 2019-01-01 - 2019-01-06]

The special case of first and last day being in the same month is handled by the loop not looping at all and the add after the loop adding the single interval to the list.

If two lists of dates are required

If you do insist on two lists, the two-arg datesUntil method is handy:

    List<LocalDate> firstDays = new ArrayList<>();
    firstDays.add(firstDayOverall);
    // first day not to be included in first days
    LocalDate endExclusive = lastDayOverall.withDayOfMonth(1).plusMonths(1);
    List<LocalDate> remainingFirstDays = firstDayOverall.withDayOfMonth(1)
            .plusMonths(1)
            .datesUntil(endExclusive, Period.ofMonths(1))
            .collect(Collectors.toList());
    firstDays.addAll(remainingFirstDays);
    System.out.println("First days: " + firstDays);

    // Calculate last days as the day before each first day except the first
    List<LocalDate> lastDays = remainingFirstDays.stream()
            .map(day -> day.minusDays(1))
            .collect(Collectors.toCollection(ArrayList::new));
    lastDays.add(lastDayOverall);
    System.out.println("Last days:  " + lastDays);

Output:

First days: [2018-12-10, 2019-01-01]
Last days:  [2018-12-31, 2019-01-06]

Aside

At first I had a feeling that the YearMonth class would make for an elegant solution. I realized, however, that it would have required special treatment of first and last month, so I don’t think it could have led to more elegant code than what we have above. You may try if you like.

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