Reputation: 7917
Consider this class:-
import java.sql.Timestamp;
public class Report {
private short value;
private Timestamp created;
//Getters, Setters
}
I have a List
of Reports obtained from the database using ORDER BY created DESC
.
The task is to have just the latest report from each month. I know it can be done at SQL level, but for some reason I need to do it in Java.
This is how I solved it:-
/**
* Assuming that the reports are sorted with <code>ORDER BY created DESC</code>, this method filters the list so
* that it contains only the latest report for any month.
*
* @param reports Sorted list of reports
* @return List containing not more than one report per month
*/
public static List<Report> oneReportPerMonthFilter(List<Report> reports) {
Map<String, Report> monthlyReports = new HashMap<>();
reports.forEach(report -> {
String yearMonth = getCreatedYearMonth(report);
if (!monthlyReports.containsKey(yearMonth)) {
monthlyReports.put(yearMonth, report);
}
});
return new ArrayList<>(monthlyReports.values());
}
private static String getCreatedYearMonth(Report report) {
return YearMonth
.from(ZonedDateTime.of(report.getCreated().toLocalDateTime(), ZoneOffset.UTC))
.toString();
}
Question 1
Although this works as expected, I had to create a Map
and then convert the values
back to a List
. Is there a better way of doing this using Java 8 Stream API? Perhaps a more "functional" way?
Question 2
Can the method getCreatedYearMonth(Report report)
which converts Timestamp
to YearMonth
be simplified? Currently it changes Timestamp
to LocalDateTime
and then to ZonedDateTime
and then to YearMonth
.
Unit test:-
@Test
public void shouldFilterOutMultipleReportsPerMonth() {
Report report1 = new Report();
report1.setCreated(Timestamp.from(Instant.EPOCH));
report1.setValue((short) 100);
Report report2 = new Report();
report2.setCreated(Timestamp.from(Instant.EPOCH.plus(10, ChronoUnit.DAYS)));
report2.setValue((short) 200);
Report report3 = new Report();
report3.setCreated(Timestamp.from(Instant.EPOCH.plus(40, ChronoUnit.DAYS)));
report3.setValue((short) 300);
List<Report> reports = Stream.of(report3, report2, report1).collect(Collectors.toList());
List<Report> filteredReportList = ExampleClass.oneReportPerMonthFilter(reports);
Assert.assertEquals(2, filteredReportList.size());
Assert.assertEquals((short) 300, (short) filteredReportList.get(0).getValue());
Assert.assertEquals((short) 200, (short) filteredReportList.get(1).getValue());
}
Edit 1
Answer
Thanks to all for your answers. Using Amith's and Johannes's answers, I was able to come up with this version, which is simple and easy to read:-
public static List<Report> oneReportPerMonthFilter(List<Report> reports) {
Set<YearMonth> found = new HashSet<>();
return reports.stream()
.filter(r -> found.add(getCreatedYearMonth(r)))
.collect(Collectors.toList());
}
private static YearMonth getCreatedYearMonth(Report report) {
return YearMonth.from(
report.getCreated()
.toInstant()
.atZone(ZoneOffset.UTC));
}
Seems like there is no quick way to convert Timestamp to YearMonth. We can take a string representation of year-month from Timestamp as shown by Amith though.
Upvotes: 0
Views: 1887
Reputation: 4884
You can use Java stream with stateful predicate like below to filter first report by month.
NOTE: - Don't run this with parallelStream(), as its not thread safe and also assumes the list is sorted by date to get desired result of picking first one in order for month & year.
HIGHLIGHTS
public static List<Report> oneReportPerMonthFilter(List<Report> reports) {
Set<String> found = new HashSet<>();
return reports.stream().filter(r -> found.add(getCreatedYearMonth(r))).collect(Collectors.toList());
}
public static String getCreatedYearMonth(Report report) {
//Or you can use SimpleDateFormat to extract Year & Month
Calendar cal = Calendar.getInstance();
cal.setTime(report.getCreated());
return "" + cal.get(Calendar.YEAR) + cal.get(Calendar.MONTH);
}
}
TESTABLE (FULL) CODE
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class ReportFilter {
public static void main(String[] args) {
Report report1 = new Report();
report1.setCreated(Timestamp.from(Instant.EPOCH));
report1.setValue((short) 100);
Report report2 = new Report();
report2.setCreated(Timestamp.from(Instant.EPOCH.plus(10, ChronoUnit.DAYS)));
report2.setValue((short) 200);
Report report3 = new Report();
report3.setCreated(Timestamp.from(Instant.EPOCH.plus(40, ChronoUnit.DAYS)));
report3.setValue((short) 300);
Report report4 = new Report();
report4.setCreated(Timestamp.from(Instant.EPOCH.plus(40, ChronoUnit.DAYS)));
report4.setValue((short) 400);
List<Report> reports = Arrays.asList(report1, report2, report3, report4);
List<Report> filteredReports = oneReportPerMonthFilter(reports);
System.out.println(filteredReports);
}
public static List<Report> oneReportPerMonthFilter(List<Report> reports) {
Set<String> found = new HashSet<>();
return reports.stream().filter(r -> found.add(getCreatedYearMonth(r))).collect(Collectors.toList());
}
public static String getCreatedYearMonth(Report report) {
//Or you can use SimpleDateFormat to extract Year & Month
Calendar cal = Calendar.getInstance();
cal.setTime(report.getCreated());
return "" + cal.get(Calendar.YEAR) + cal.get(Calendar.MONTH);
}
}
class Report {
private Timestamp created;
private short value;
public Timestamp getCreated() {
return created;
}
public void setCreated(Timestamp created){
this.created = created;
}
public short getValue() {
return value;
}
public void setValue(short value) {
this.value = value;
}
@Override
public String toString() {
return "Report [created=" + created + ", value=" + value + "]";
}
}
Upvotes: 1
Reputation: 2613
To answer your first question:
Using the standard API there isn't really a way to do what you want without creating a map; however, I did re-write to make it more idiomatic.
public static List<Report> oneReportPerMonthFilter2(List<Report> reports) {
return reports.stream()
.collect(Collectors.groupingBy(Q50938904::getCreatedYearMonth))
.values().stream()
.map(p-> p.get(0))
.collect(Collectors.toList());
}
If you are able, consider looking at StreamEx. Its an extension of the streams API.
Upvotes: 1
Reputation: 15163
You should not convert the YearMonth
to a string. Just omit the toString()
part. Also, I was able to simplify it to this:
private static YearMonth getCreatedYearMonth(Report report) {
return YearMonth.from(report.getCreated().toInstant().atZone(ZoneOffset.UTC));
}
To get the result you want, you have to chain some Collectors:
Map<YearMonth, Report> last = reports.stream()
.collect(Collectors.groupingBy(r -> getCreatedYearMonth(r),
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(Report::getCreated)),
Optional::get)));
The outer Collector
is groupingBy
: We want something from each YearMonth
. The downstream collector will only see Report
s in the same month.
The downstream Collector
is collectingAndThen
, because the maxBy
collector will result in a Optional<Report>
. But we already know that there is at least one Report
for each month, so we just unwrap it.
The innermost collector is just getting the maximum value by the Timestamp
.
Upvotes: 4