3
\$\begingroup\$

I have a StudentSchedule class that contains schedules of a student, the student may switch between rooms over time. There's no overlap with the date ranges so if a student stops at 2020-01-01 the next record would be 2020-01-02.

Given the StudentSchedule, I want a ProgramEnrollment which disregards room changes and coalesces contiguous StudentSchedules.

So given the following StudentSchedules

 new StudentSchedule("A", "1", parse("2020-01-01"), parse("2020-01-02")), new StudentSchedule("B", "1", parse("2020-01-06"), parse("2020-01-10")), new StudentSchedule("B", "2", parse("2020-01-11"), null), new StudentSchedule("A", "2", parse("2020-01-03"), parse("2020-01-04")) 

I want a result like

A 2020-01-01 - 2020-01-04 B 2020-01-06 - null 

I have extracted the relevant code below. What I want to do is see if I can change the computeProgramEnrollments() to use more functions or streaming API including groupBy then flatmap then collect to a new list. Assuming it is possible.

import java.time.*; import java.util.*; import java.util.stream.*; import static java.time.LocalDate.*; class Main { public static void main(String[] args) { System.out.println(computeProgramEnrollments()); } static Stream<StudentSchedule> schedules() { return Stream.of( new StudentSchedule("A", "1", parse("2020-01-01"), parse("2020-01-02")), new StudentSchedule("B", "1", parse("2020-01-06"), parse("2020-01-10")), new StudentSchedule("B", "2", parse("2020-01-11"), null), new StudentSchedule("A", "2", parse("2020-01-03"), parse("2020-01-04")) ); } public static List<ProgramEnrollment> computeProgramEnrollments() { List<ProgramEnrollment> ret = new ArrayList<>(); String currentProgram = null; LocalDate currentStartDate = null; LocalDate currentStopDate = null; boolean newEnrollmentRequired = false; for (final StudentSchedule schedule : schedules().sorted(Comparator.comparing(StudentSchedule::getStartDate)).collect(Collectors.toList())) { if (Objects.equals(currentProgram, schedule.getProgram())) { if (currentStopDate != null && currentStopDate.plusDays(1).isEqual(schedule.getStartDate())) { // continuation currentStopDate = schedule.getStopDate(); } else { newEnrollmentRequired = true; } } else { newEnrollmentRequired = true; } if (newEnrollmentRequired) { if (currentProgram != null) { final ProgramEnrollment e = new ProgramEnrollment(currentProgram, currentStartDate, currentStopDate ); ret.add(e); } currentProgram = schedule.getProgram(); currentStartDate = schedule.getStartDate(); currentStopDate = schedule.getStopDate(); newEnrollmentRequired = false; } } if (currentProgram != null) { final ProgramEnrollment e = new ProgramEnrollment(currentProgram, currentStartDate, currentStopDate ); ret.add(e); } return ret; } } class StudentSchedule { String program; String room; LocalDate start; LocalDate stop; public StudentSchedule(String program, String room, LocalDate start, LocalDate stop) { this.program = program; this.room = room; this.start = start; this.stop = stop; } public String getProgram() { return program; } public String getRoom() { return room; } public LocalDate getStartDate() { return start; } public LocalDate getStopDate() { return stop; } } class ProgramEnrollment { String program; LocalDate start; LocalDate stop; public ProgramEnrollment(String program, LocalDate start, LocalDate stop) { this.program = program; this.start = start; this.stop = stop; } public String getProgram() { return program; } public LocalDate getStartDate() { return start; } public LocalDate getStopDate() { return stop; } public String toString() { return program + " " + start + "-" + stop + "\n"; } } 

https://repl.it/@trajano/StupendousBetterEngines

\$\endgroup\$

    2 Answers 2

    2
    \$\begingroup\$

    This is a nice candidate for group-by/map-reduce!

    (we need an additional stream with map to get rid of the optional that is standard in reducing)

    Idea

    The idea is to group each StudentSchedule by program, map them to a small ProgramEnrollment, then to reduce the values of each key by merge or coalesce these ProgramEnrollments and finally return a list of the reduced values.

    Note that the reducing requires at least one entry in the stream to prevent Optional.empty()

    You could even improve by returning a Stream<ProgramEnrollment>.

    Note that it is possible to inline the reductor and mapper, but for clarity I introduced some local values.

    Code

    public static List<ProgramEnrollment> computeProgramEnrollments() { Collector<ProgramEnrollment, ?, Optional<ProgramEnrollment>> reductor = Collectors.reducing(ProgramEnrollment::merge); Collector<StudentSchedule, ?, Optional<ProgramEnrollment>> mapper = Collectors.mapping(ProgramEnrollment::from, reductor); return schedules() .sorted(Comparator.comparing(StudentSchedule::getStartDate)) .collect(Collectors.groupingBy(StudentSchedule::getProgram, mapper)) .values() .stream() .map(Optional::get).collect(Collectors.toList()); } 

    Code - using toMap

    toMap allows for easier reducing and helps to get rid of the Optional.

    public static List<ProgramEnrollment> computeProgramEnrollments() { return schedules() .sorted(Comparator.comparing(StudentSchedule::getStartDate)) .collect(Collectors.toMap(StudentSchedule::getProgram, ProgramEnrollment::from, ProgramEnrollment::merge)) .values() .stream() .collect(Collectors.toList()); } 

    With some additional methods here (they could also be extracted to a util / converted to static methods on Main) :

    static class ProgramEnrollment { ... public static ProgramEnrollment from(StudentSchedule s) { return new ProgramEnrollment(s.getProgram(), s.getStartDate(), s.getStopDate()); } public ProgramEnrollment merge(ProgramEnrollment e) { LocalDate minStart = this.start == null ? e.start : e.start == null ? this.start : e.start.isBefore(this.start) ? e.start : this.start; LocalDate maxStop = this.stop == null ? null : e.stop == null ? null : e.stop.isAfter(this.stop) ? e.stop : this.stop; return new ProgramEnrollment(this.program, minStart, maxStop); } } 
    \$\endgroup\$
    3
    • \$\begingroup\$Not sure but I think your reducer does not handle the plusDays(1)\$\endgroup\$CommentedMay 8, 2020 at 13:53
    • \$\begingroup\$It does not seem to handle the gaps e.g. StudentSchdule(A, 2020-01-01, 2020-01-02), StudentSchdule(A, 2020-01-05, 2020-01-10) should have two resulting entries.\$\endgroup\$CommentedMay 8, 2020 at 14:47
    • 1
      \$\begingroup\$Oh I see. Good point! I didn't get that straight from the requirements. In that case, the reduce step should be replaced by a merge step that merges all the periods in one go, not too hard because the are sorted. If I find time I will fix it 👍\$\endgroup\$CommentedMay 8, 2020 at 15:53
    1
    \$\begingroup\$

    This uses collect since reduce is meant for collecting with an immutable result and this is validated to use with parallel streams.

    return schedules() .map(s->ProgramEnrollment.from(s)) // same as @RobAu .sorted(Comparator.comparing(ProgramEnrollment::getStartDate)) .collect( ArrayList::new, (c, e)->{ if (c.isEmpty()) { c.add(e); } else { var top = c.get(c.size() - 1); if (!top.getProgram().equals(e.getProgram())) { // Program changed c.add(e); } else if (top.getStopDate() != null && top.getStopDate().plusDays(1).isBefore(e.getStartDate())) { // At this point there is a gap with the program c.add(e); } else if (top.getStopDate() != null && top.getStopDate().plusDays(1).isEqual(e.getStartDate())) { // update the stop date with the new stop date top.setStopDate(e.getStopDate()); } else { throw new IllegalStateException(); } } }, (c1, c2) -> { var topC1 = c1.get(c1.size() - 1); var botC2 = c2.get(0); if (!topC1.getProgram().equals(botC2.getProgram()) || topC1.getStopDate() != null && topC1.getStopDate().plusDays(1).isBefore(botC2.getStartDate())) { // Program changed or there is a gap with the program c1.addAll(c2); } else if (topC1.getStopDate() != null && topC1.getStopDate().plusDays(1).isEqual(botC2.getStartDate())) { // update the stop date with the new stop date botC2.setStartDate(topC1.getStartDate()); c1.remove(c1.size() - 1); c1.addAll(c2); } else { // handle cases when the data does not match the preconditions throw new IllegalStateException(); } } ); 
    \$\endgroup\$

      Start asking to get answers

      Find the answer to your question by asking.

      Ask question

      Explore related questions

      See similar questions with these tags.