IceTeaGreen
IceTeaGreen

Reputation: 153

Java 8 api streams. Need to resolve duplicate keys issue

I have a log file which looks like this:

LSR2019-07-12_12:07:21.554
KMH2019-07-12_12:09:44.291
RGH2019-07-12_12:29:28.352
RGH2019-07-12_12:33:08.603

I have a parser which parses data to abbreviation/date/time:

public Map <String, ?> parse() throws IOException {
        
        try (Stream<String>lines = Files.lines(path)){
            

        return lines.collect(Collectors.toMap(
                string -> string.substring(0,3),
                string -> new DateAndTimeInfo(LocalTime.from(DateTimeFormatter.ofPattern("HH:mm:ss.SSS").parse((string.substring(3).split("_")[1]))),
                        LocalDate.parse(string.substring(3).split("_")[0], DateTimeFormatter.ofPattern("yyyy-MM-dd"))),
                (string1, string2)-> ??? )); //Struggle here

After parsing it creates a map that contains abbreviations as keys and instances of DateAndTimeInfo class. The class looks like this:

public class DateAndTimeInfo {
    private List<LocalTime> localTime;
    private List<LocalDate> localDate;
    
    public DateAndTimeInfo(LocalTime localTime, LocalDate localDate) {
        this.localTime = Arrays.asList(localTime);
        this.localDate = Arrays.asList(localDate);
    }
    public List<LocalTime> getLocalTime() {
        return this.localTime;
    }
    public List<LocalDate> getLocalDate() {
        return this.localDate;
    }
    public void addAnotherLapTime(LocalTime localtime, LocalDate localDate) {
        this.localTime.add(localtime);
        this.localDate.add(localDate);
    }
}

Everything works fine until the log file has a duplicate abbreviation. As soon as a duplicate key appears I want the data to be stored inside the DateAndTimeInfo object, which was created when the first duplicate was parsed. To do so I have the addAnotherLapTime() method. The problem is I can't figure out how to write it in my stream.

Upvotes: 2

Views: 333

Answers (2)

WJS
WJS

Reputation: 40047

You can try this. I added lambdas to make it a little cleaner to view. Basically it uses the merge function of toMap to copy list from the newly created class to the already existing class. Here are the mods I made to your class.

  • the constructor puts the values in the lists.
  • added a copy constructor to copy one list to the other in another instance of DateAndTimeInfo
  • Added a toString method.
    String[] lines = {
            "LSR2019-07-12_12:07:21.554",
            "KMH2019-07-12_12:09:44.291",
            "KMH2019-07-12_12:09:44.292",
            "RGH2019-07-12_12:29:28.352",
            "RGH2019-07-12_12:33:08.603",
             "RGH2019-07-12_12:33:08.604"};

Function<String, LocalTime> toLT = (str) -> LocalTime
            .from(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
                    .parse((str.substring(3).split("_")[1])));
    
    
Function<String, LocalDate> toLD = (str) -> LocalDate.parse(
            str.substring(3).split("_")[0],
            DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    
    
Map<String, DateAndTimeInfo> map = lines
        .collect(Collectors.toMap(
            string -> string.substring(0,3),
            string-> new DateAndTimeInfo(toLT.apply(string), toLD.apply(string)),
            (dti1,dti2)-> dti1.copy(dti2)))


class DateAndTimeInfo {
      private List<LocalTime> localTime = new ArrayList<>();
      private List<LocalDate> localDate = new ArrayList<>(); 
        
    public DateAndTimeInfo(LocalTime lt, LocalDate ld) {
        localTime.add(lt);
        localDate.add(ld);
    }

    public DateAndTimeInfo copy(DateAndTimeInfo dti) {
        this.localTime.addAll(dti.localTime);
        this.localDate.addAll(dti.localDate);
        return this;
    }
    public String toString() {
         return localTime.toString() + "\n    " + localDate.toString();  
    }
}

For the given test data, it prints.

RGH=[12:29:28.352, 12:33:08.603, 12:33:08.604]
    [2019-07-12, 2019-07-12, 2019-07-12]
KMH=[12:09:44.291, 12:09:44.292]
    [2019-07-12, 2019-07-12]
LSR=[12:07:21.554]
    [2019-07-12]

Note. Did you consider of creating a map like the following: Map<String, List<DateAndTimeInfo>> and storing just the date and time in each class as fields? You could get them with getters. It would be trivial to implement. So the value of the key would be a list of DateAndTimeInfo objects.

Map<String, List<DateAndTimeInfo>> map = lines
                .collect(Collectors.groupingBy(str->str.substring(0,3),
                        Collectors.mapping(str->new DateAndTimeInfo(toLT.apply(str),
                                toLD.apply(str)), Collectors.toList())));

Upvotes: 1

Holger
Holger

Reputation: 298539

Since the combination of values will end up in List objects, this is a task for the groupingBy collector.

But first, you have to fix the DateAndTimeInfo class. It’s only constructor

public DateAndTimeInfo(LocalTime localTime, LocalDate localDate) {
    this.localTime = Arrays.asList(localTime);
    this.localDate = Arrays.asList(localDate);
}

creates fixed size lists, so the method

    public void addAnotherLapTime(LocalTime localtime, LocalDate localDate) {
        this.localTime.add(localtime);
        this.localDate.add(localDate);
    }

will fail with exceptions.

When you use

public class DateAndTimeInfo {
    private List<LocalTime> localTime;
    private List<LocalDate> localDate;

    public DateAndTimeInfo() {
        localTime = new ArrayList<>();
        localDate = new ArrayList<>();
    }
    public List<LocalTime> getLocalTime() {
        return this.localTime;
    }
    public List<LocalDate> getLocalDate() {
        return this.localDate;
    }
    public void addAnotherLapTime(LocalTime localtime, LocalDate localDate) {
        this.localTime.add(localtime);
        this.localDate.add(localDate);
    }
}

instead, you can collect the map like

public Map<String, DateAndTimeInfo> parse() throws IOException {
    DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss.SSS");
    try(Stream<String>lines = Files.lines(path)){
        return lines.collect(Collectors.groupingBy(
            string -> string.substring(0,3),
            Collector.of(DateAndTimeInfo::new, (info,str) -> {
                LocalDateTime ldt = LocalDateTime.parse(str.substring(3), f);
                info.addAnotherLapTime(ldt.toLocalTime(), ldt.toLocalDate());
            }, (info1,info2) -> {
                info1.getLocalDate().addAll(info2.getLocalDate());
                info1.getLocalTime().addAll(info2.getLocalTime());
                return info1;
        })));
    }
}

groupingBy allows to specify a collector for the groups and this solution creates a new ad-hoc Collector for the DateAndTimeInfo objects.

You may consider whether you really want to keep the dates and times in different lists.

The alternative would be:

public class DateAndTimeInfo {
    private List<LocalDateTime> localDateTimes;

    public DateAndTimeInfo(List<LocalDateTime> list) {
        localDateTimes = list;
    }
    public List<LocalDateTime> getLocalDateTimes() {
        return localDateTimes;
    }
    // in case this is really needed
    public List<LocalTime> getLocalTime() {
        return localDateTimes.stream()
            .map(LocalDateTime::toLocalTime)
            .collect(Collectors.toList());
    }
    public List<LocalDate> getLocalDate() {
        return localDateTimes.stream()
            .map(LocalDateTime::toLocalDate)
            .collect(Collectors.toList());
    }
}

and then

public Map<String, DateAndTimeInfo> parse() throws IOException {
    DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss.SSS");
    try(Stream<String>lines = Files.lines(path)){
        return lines.collect(Collectors.groupingBy(
            string -> string.substring(0,3),
            Collectors.collectingAndThen(
                Collectors.mapping(s -> LocalDateTime.parse(s.substring(3), f),
                    Collectors.toList()),
                DateAndTimeInfo::new)));
    }
}

Upvotes: 4

Related Questions