11

I have a List of items with a (java.util.)Date property, and I want to create a DataSeriesItem for each day beginning from the oldest date up to now. It is for a chart series with a timeline.

The creation of that DataSeriesItem will look like this:
DataSeriesItem seriesItem = new DataSeriesItem(Date, occurrenceCount);
Where the occurrenceCount is the count of Items where their Date property matches that day. The first parameter can also be of type java.time.Instant

I have managed to find a way that works, but I am certain that my approach is very bad and could possibly be done with one stream, maybe two. However, I am a beginner in streams and could not do it with my knowledge.

Is this possible with stream? How would it probably look like approximately?
I'm not asking you to actually do my whole implementation anew, but only point me to the correct streamfunctions and mappings that you would use, and for bonus points an example of it.

Here is my ugly solution:

List<?> items = myItems;
Collection<Date> foundDates = new HashSet<>();

for (Object item : items) {
    foundDates.add((Date)getPropertyValueFromItem(item, configurator.getDateProperty()));
}

//======  This is the part I am asking about ======//

Map<Instant, Integer> foundInstants = new HashMap<>();
foundDates.stream().sorted(Date::compareTo).forEach(date -> {
    Calendar c = Calendar.getInstance();
    c.clear(); // clear nanoseconds, or else equals won't work!
    c.set(date.getYear()+1900, date.getMonth(), date.getDate(), 0, 0, 0);
    if(!foundInstants.containsKey(c.toInstant())){
        foundInstants.put(c.toInstant(), 1);
    } else {
        // increment count of that entry
        Integer value = foundInstants.get(c.toInstant());
        foundInstants.remove(c.toInstant());
        foundInstants.put(c.toInstant(), ++value);
    }
});

//====== Leaving this code here for context ======//  
// Could this maybe simplyfied too by using streams  ?

// find oldest date
Date dateIndex = foundDates.stream().min(Date::compareTo).get();
Date now = new Date();

// starting from oldest date, add a seriesItem for each day until now
// if dateOccurrences contains the current/iterated date, use it's value, else 0
while(dateIndex.before(now)){
    Calendar c = Calendar.getInstance();
    c.clear();// clear nanoseconds, or else equals won't work!
    c.set(dateIndex.getYear()+1900, dateIndex.getMonth(), dateIndex.getDate(), 0, 0, 0);

    if(foundInstants.containsKey(c.toInstant())){
        ExtendedDataSeriesItem seriesItem = new ExtendedDataSeriesItem(c.toInstant(), foundInstants.get(c.toInstant()));
        seriesItem.setSeriesType("singleDataPoint");
        series.add(seriesItem);
    } else {
        ExtendedDataSeriesItem seriesItem = new ExtendedDataSeriesItem(c.toInstant(), 0);
        seriesItem.setSeriesType("singleDataPoint");
        series.add(seriesItem);
    }
    c.add(Calendar.DATE, 1); // adding a day is complicated. Calendar gets it right. Date does not. This is why I don't use Date here
    dateIndex = c.getTime();
}
kscherrer
  • 5,486
  • 2
  • 19
  • 59

2 Answers2

9

You can use groupingBy() and then use the downstream collector counting().

Map<Date, Long> occurrances = dateList.stream().collect(
                  groupingBy(d -> yourTransformation(d), counting()));

It should be easy enough to create your DataSeriesItem objects from that map.

daniu
  • 14,137
  • 4
  • 32
  • 53
  • That seems to be exactly what I was looking for, thank you so much. – kscherrer Jan 11 '19 at 14:17
  • Works like a charm (I had to write `Collectors.groupingBy` else it wouldn't recognize it). I used basically [this](https://stackoverflow.com/a/1908419/3441504) for `yourTransformation(d)` except converting it to Date at the end instead of long. – kscherrer Jan 11 '19 at 15:34
  • @Cashbee you can use a static import so you don't need the `Collectors.`. I do that because it's more readable, and do so in answers here as well. – daniu Jan 11 '19 at 17:56
5

To count you're looking for something like:

Map<Instant, Long> foundInstants =  foundDates.stream()
            .collect(Collectors.groupingBy(Date::toInstant, Collectors.counting()));

to add to that you could cut short those if..else into :

ExtendedDataSeriesItem seriesItem = 
        new ExtendedDataSeriesItem(c.toInstant(), foundInstants.getOrDefault(c.toInstant(), 0L));
seriesItem.setSeriesType("singleDataPoint");
series.add(seriesItem);

and this goes by saying that you should at the same time look for migrating to LocalDateTime and refrain from using Date.

Naman
  • 27,789
  • 26
  • 218
  • 353
  • That looks very promising and seems to be exactly what I was looking for. I'll need time to test it. daniu was first with kind of the same answer so the tick goes to him. Thanks nevertheless! @Lino nullpointer is right :) – kscherrer Jan 11 '19 at 14:13
  • @Cashbee nevermind, additionally shortened your `if..else` as well – Naman Jan 11 '19 at 14:19
  • 1
    After this hell of a day trying to convert Date into Calendar into Instant just because Date cant do addDays(1), there is no way I keep using Date :) – kscherrer Jan 11 '19 at 14:26