Grouping the list of list based on the property - java

Below are my sample Class Object,
class Sample{
String name;
String value;
String id;
}
My Input List looks like below,
[Sample(name=title, value=Title, id=1), Sample(name=input, value=DCE, id=1), Sample(name=output, value=CDE, id=1), Sample(name=title, value=Title, id=2), Sample(name=input, value=DCEE, id=2), Sample(name=output, value=CDEE, id=2), Sample(name=title, value=Title, id=3), Sample(name=input, value=DCEEE, id=3), Sample(name=output, value=CDEEE, id=3)]
I need to group the list of list based on the id property and display the value alone as the below.
Expected Output:
Title DCE CDE 1
Title DCEE CDEE 2
Title DCEEE CDEEE 3
Expected Output:
{[Title DCE CDE 1],
Title DCEE CDEE 2],
Title DCEEE CDEEE 3]}

Contrary to the comments to the original post saying that there's no question here, I think it's clear enough what the OP (original poster) is asking. But there is some uncertainty introduced by the inclusion of two expected outputs. I'm going to assume it's a list of strings, where the string is a concatenation of the Sample.value for each sample with the same id value. (I agree that you should edit the original post to make this clear.)
Assuming the OP meant this (I'll use record instead of class because it looks like these are immutable objects):
record Sample(
String name,
String value,
String id) {
}
Sample[] samples = new Sample[] {
new Sample("title", "Title", "1"),
new Sample("input", "DCE", "1"),
new Sample("output", "CDE", "1"),
new Sample("title", "Title", "2"),
new Sample("input", "DCEE", "2"),
new Sample("output", "CDEE", "2"),
new Sample("title", "Title", "3"),
new Sample("input", "DCEEE", "3"),
new Sample("output", "CDEEE", "3")
};
and the task is to use the Java streaming APIs to get this result:
[ "Title DCE CDE 1",
"Title DCEE CDEE 2",
"Title DCEEE CDEEE 3"
]
the approach is to use Collectors.groupingBy(Sample::id) to initially transform to an intermediate result of a Map<String, List<Sample>>
1=[Sample[name=title, value=Title, id=1], Sample[name=input, value=DCE, id=1], Sample[name=output, value=CDE, id=1]]
2=[Sample[name=title, value=Title, id=2], Sample[name=input, value=DCEE, id=2], Sample[name=output, value=CDEE, id=2]]
3=[Sample[name=title, value=Title, id=3], Sample[name=input, value=DCEEE, id=3], Sample[name=output, value=CDEEE, id=3]]
Note that for each key, the value is a list. In order to extract the sample.value from each sample in the list, you would then stream the entrySet, map each value by streaming the list and mapping each sample to just the value property (map(Sample::value)), and then collect those into a string using Collectors.joining(" ") + " " + entry.getKey()). And then you would .collect(Collectors.toList()) to get your expected output. In pseudo code, you finish the transformation like this:
intermediateResult.entrySet().stream()
.map(entry -> stream the entry.value samples list
.map(each sample to just the sample.value)
.collect(joining with delimiter = " ") and
concatenating " " + entry.getKey() to the end)
.collect(each of these strings to a list);
You could also do this transformation in one statement using collectingAndThen():
List<String> result = Arrays.stream(samples)
.collect(Collectors.collectingAndThen(
initialTransformer,
finishingTransformer));
I haven't filled in everything so that you can have the challenge of completing it and gaining a deeper understanding.

Related

Join Custom lists in java

I have two custom lists as follows.
List<OfficeName> = [{id: 1, offname: "Office1"}{id: 2, offname: "Office2"}]
List<OfficeLocation> = [{id: 1, offlocation: "location1"}{id: 2, offlocation: "locatio2"}]
I want result as follows:
list<OfficeDetails> = [{id: 1, offname: "Office1",offlocation: "location1" },
{id: 2, offname: "Office2", offlocation: "location2"}]
The first two lists needs to be joined on basis of "id" to give a new list which is equivalent to the join operation in sql tables.
My model classes are
public class OfficeName {
int id;
String offname;
//getter and setter
}
.................
public class OfficeLocation{
int id;
String offlocation;
//getter and setter
}
.........
Currently I am Iterating and manually adding as follows to a LinkedHashSet .
{
List<OfficeName> officeName = new ArrayList<OfficeName>();
onr.findById(id).forEach(officeName::add); // adding values from auto wired Repository
List<OfficeLocation> officeLocation = new ArrayList<OfficeLocation>();
olr.findById(id).forEach(officeLocation::add); // adding values from auto wired Repository
LinkedHashSet<LinkedHashSet<String>> lhs = new LinkedHashSet<LinkedHashSet<String> >();
OfficeName officeName1 = new OfficeName();
OfficeLocation officeLocation1 = new OfficeLocation();
Iterator<OfficeName> onIterator = officeName.iterator();
Iterator<OfficeLocation> olIterator = officeLocation.iterator();
while (onIterator.hasNext()) {
officeName1 =onIterator.next();
int idon =officeName1.getId();
while(olIterator.hasNext()){
officeLocation1 = olIterator.next();
int idol = officeLocation1.getId();
if(idon==idol)
{
lhs.add(new LinkedHashSet<String>(Arrays.asList( String.valueOf(officeName1.getId()),officeName1.getOffname(),officeLocation1.getOfflocation())));
olIterator.remove();
break;
}
};
}
I am not sure whether this is correct way to achieve the same as I am new to java. In C#, this could able to achieve through data tables. Please suggest whether there is any faster way?
Assuming both input lists:
Are distinct, with no duplicate id values in either, and…
Are complete, with a single object in both lists for each possible id value
… then we can get the work done with little code.
I use NavigableSet or SortedSet implementations to hold our input lists, the names and the locations. Though I have not verified, I assume being sorted will yield better performance when searching for a match across input collections.
To get the sorting done, we define a Comparator for each input collection: Comparator.comparingInt( OfficeName :: id ) & Comparator.comparingInt( OfficeLocation :: id ) where the double-colons make a method reference. To each NavigableSet we add the contents of our inputs, an unmodifiable list made with the convenient literals syntax of List.of.
To get the actual work done of joining these two input collections, we make a stream of either input collection. Then we produce a new object of our third joined class using inputs from each element of the stream plus its counterpart found via a stream of the other input collection. These newly produced objects of the third joined class are then collected into a list.
NavigableSet < OfficeName > officeNames = new TreeSet <>( Comparator.comparingInt( OfficeName :: id ) );
officeNames.addAll( List.of( new OfficeName( 1 , "Office1" ) , new OfficeName( 2 , "Office2" ) ) );
NavigableSet < OfficeLocation > officeLocations = new TreeSet <>( Comparator.comparingInt( OfficeLocation :: id ) );
officeLocations.addAll( List.of( new OfficeLocation( 1 , "location1" ) , new OfficeLocation( 2 , "locatio2" ) ) );
List < Office > offices = officeNames.stream().map( officeName -> new Office( officeName.id() , officeName.name() , officeLocations.stream().filter( officeLocation -> officeLocation.id() == officeName.id() ).findAny().get().location() ) ).toList();
Results:
officeNames = [OfficeName[id=1, name=Office1], OfficeName[id=2, name=Office2]]
officeLocations = [OfficeLocation[id=1, location=location1], OfficeLocation[id=2, location=locatio2]]
offices = [Office[id=1, name=Office1, location=location1], Office[id=2, name=Office2, location=locatio2]]
Our three classes, the two inputs and the third joined one, are all written as records here for their convenient brevity. This Java 16+ feature is a brief way to declare a class whose main purpose is to communicate data transparently and immutably. The compiler implicitly creates the constructor, getters, equals & hashCode, and toString. Note that a record can be defined locally as well as nested or separate.
public record OfficeName( int id , String name ) { }
public record OfficeLocation( int id , String location ) { }
public record Office( int id , String name , String location ) { }
Given the conditions outlined above, we could optimize by hand-writing loops to manage the matching of objects across the input collections, rather than using streams. But I would not be concerned about the performance impact unless you had huge amounts of data that had proven to be a bottleneck. Otherwise, using streams makes for less code and more fun.
One of the lists (e.g. locations) should be converted into a map (HashMap) by a key on which the joining should be made, in this case id field.
Then, assuming that OfficeDetails class has an all-args constructor, the resulting list may be retrieved by streaming the other list offices and mapping its contents into new OfficeDetails, filling the remaining location argument by looking up the map.
List<OfficeName> offices = Arrays.asList(
new OfficeName(1, "Office1"), new OfficeName(2, "Office2"), new OfficeName(3, "Office3")
);
List<OfficeLocation> locations = Arrays.asList(
new OfficeLocation(1, "Location 1"), new OfficeLocation(2, "Location 2"), new OfficeLocation(4, "Location 4")
);
Map<Integer, OfficeLocation> mapLoc = locations
.stream()
.collect(Collectors.toMap(
OfficeLocation::getId,
loc -> loc,
(loc1, loc2) -> loc1 // to resolve possible duplicates
));
List<OfficeDetails> details = offices
.stream()
.filter(off -> mapLoc.containsKey(off.getId())) // inner join
.map(off -> new OfficeDetails(
off.getId(), off.getOffname(),
mapLoc.get(off.getId()).getOfflocation() // look up the map
))
.collect(Collectors.toList());
details.forEach(System.out::println);
Output (assuming toString is implemented in OfficeDetails):
{id: 1, offname: "Office1", offlocation: "Location 1"}
{id: 2, offname: "Office2", offlocation: "Location 2"}
If offices list is not filtered by condition mapLoc.containsKey, an implementation of LEFT JOIN is possible (when null locations are stored in the resulting OfficeDetails).
To implement RIGHT JOIN (with null office names and all available locations), a lookup map should be created for offices, and main iteration has to be run for locations list.
To implement FULL JOIN (where either name or location parts of OfficeDetails can be null), two maps need to be created and then joined:
Map<Integer, OfficeName> mapOff = offices
.stream()
.collect(Collectors.toMap(
OfficeName::getId,
off -> off,
(off1, off2) -> off1, // to resolve possible duplicates
LinkedHashMap::new
));
List<OfficeDetails> fullDetails = Stream.concat(mapOff.keySet().stream(), mapLoc.keySet().stream())
.distinct()
.map(id -> new OfficeDetails(
id,
Optional.ofNullable(mapOff.get(id)).map(OfficeName::getOffname).orElseGet(()->null),
Optional.ofNullable(mapLoc.get(id)).map(OfficeLocation::getOfflocation).orElseGet(()->null)
))
.collect(Collectors.toList());
fullDetails.forEach(System.out::println);
Output:
{id: 1, offname: "Office1", offlocation: "Location 1"}
{id: 2, offname: "Office2", offlocation: "Location 2"}
{id: 3, offname: "Office3", offlocation: null}
{id: 4, offname: null, offlocation: "Location 4"}

String vs StringBuilder

I have a condition like :
public String createId(List<String> list)
{
String id="";
if(list.contains("name"))
id+="TEST VALUE NAME";
if(list.contains("age"))
id+="Test Value AGE";
.
.
. likewise many if condition
return id;
}
As per my understanding we should use StringBuilder in loop condition and String in simple concatenation. So here wanted to ask I should use String or StringBuilder? Kindly suggest
StringBuilder is the best for this scenario because it's mutable. the String is immutable so when you modify the string it creates a new object.
It seems that for the given task it would be better to get rid of the multiple duplicated if statements by defining a list of the keys to match the input list and use Stream API to generate the string id, e.g. Collectors.joining with delimiter or without the delimiter.
Assuming that there is a single rule to create a part of the id: append "Test Value " + key.toUpperCase(), the implementation may look as follows:
final List<String> keys = Arrays.asList(
"name", "age" /* and other needed keys*/
);
public String createId(List<String> list) {
return keys
.stream()
.filter(list::contains)
.map(String::toUpperCase)
.map(str -> "Test Value " + str)
.collect(Collectors.joining("_")); // or Collectors.joining()
}
System.out.println(createId(Arrays.asList("age", "name", "surname")));
// output: Test Value NAME_Test Value AGE
If custom parts should be provided for name, age, etc., a Map of matches should be prepared and used, also it may make sense to convert the input list into Set<String to facilitate look-ups:
final Map<String, String> keys = new LinkedHashMap<>(); {
// fill the map in special order
keys.put("name", "Name Part");
keys.put("age", "Test Age");
/* and other needed keys*/
}
public String createId(List<String> list) {
Set<String> words = new HashSet<>(list);
return keys.keySet()
.stream()
.filter(words::contains) // faster lookup O(1) at the cost of another collection
.map(keys::get)
.collect(Collectors.joining("_")); // or Collectors.joining()
}
System.out.println(createId(Arrays.asList("age", "surname", "name")));
// output: Name Part_Test Age
In general your understanding is correct about when to use String concatenation vs StringBuilder. The Java Language Specification says
To increase the performance of repeated string concatenation, a Java
compiler may use the StringBuffer class or a similar technique to
reduce the number of intermediate String objects that are created by
evaluation of an expression.
For the larger majority of cases you should use whichever method results in better readability and maintainability.

How transform a list of objects to a list of strings based on the value of a property in java?

Is there a way how to transform a list of objects to a list of strings based on the value of a property? I have an entity Tag
public class Tag {
private int tagID;
private String description;
}
I get a list of tags with their ids and descriptions:
[Tag [tagID=1, description=121], Tag [tagID=1, description=244], Tag [tagID=1, description=331], Tag [tagID=2, description=244], Tag [tagID=2, description=122]]
And what I need is:
List<String> output = ["121,244,331", "244,122"]
So far I put together this:
String description = tags.stream().map(Tag::getDescription).collect(Collectors.joining( ";" ));
outputting a result for one tag
String description = "121,244,331"
Of course, I could run it through a loop and append the result to an array, but I wondered if there is a more ellegant way - even a one-liner?
You can use Collectors.groupingBy to group by tag id and then join description using Collectors.joining
List<String> res = new ArrayList<>(tagList.stream().collect(
Collectors.groupingBy(Tag::getTagID,
Collectors.mapping(Tag::getDescription, Collectors.joining(",")))).values());
I think you are looking to:
List<String> result = tags.stream()
.collect(Collectors.groupingBy(Tag::getTagID))
.values()
.stream()
.map(t -> t.stream().map(Tag::getDescription).collect(Collectors.joining( ";" )))
.collect(Collectors.toList());
Output
[121;244;331, 244;122]

Java loop with a replaceFirst Method

this is my first post on Stack Overflow, so please, be forgiving! :)
I have a list with 403 Polish registration plates symbols and counties. It looks like this:
BIA powiat białostocki
BBI powiat bielski
BGR powiat grajewski
CT Toruń
etc.
I made a code which let me to turn the first space into "=".
import java.io.*;
public class Test {
public static void main(String args[]) {
String Str = new String("BAU powiat augustowski");
System.out.println(Str.replaceFirst(" ", "="));
}
}
How can I make a loop (for? do while?) to change all 403 records? I would appreciate any help. Thank you in advance!
If your list is a List<String>, you can do this :
for (for int i = 0, i < yourList.size(), i++) {
yourList.set(i, yourList.get(i).replaceFirst(" ", "="));
}
Other ways to loop are available here : https://crunchify.com/how-to-iterate-through-java-list-4-way-to-iterate-through-loop/
Best
You COULD also use Stream API. Eg if you want to filter all invalid strings
List<String> registrations = new ArrayList<>(5);
registrations.add("BIA powiat białostocki");
registrations.add("BBI powiat bielski");
registrations.add("BGR powiat grajewski");
registrations.add("BGGHND");
registrations.add("CT Toruń etc.");
registrations = registrations.stream()
.filter(registration -> registration.split(" ").length>1)
.map(registration -> registration.replaceFirst(" ","="))
.collect(Collectors.toList());
Output:
BIA=powiat białostocki
BBI=powiat bielski
BGR=powiat grajewski
CT=Toruń etc.
As you mentioned how can make a loop then I would suggest to learn java loop syntax at first. Following tutorial can be helpful
https://books.trinket.io/thinkjava2/chapter6.html
https://www.codingame.com/playgrounds/6162/6-ways-to-iterate-or-loop-a-map-in-java
Regarding the solution, you can loop your lists using for/while loop or using Stream API. Here is your solution using stream API:
List<String> lists = new ArrayList<>();
lists.add("BAU powiat augustowski");
lists.add("BBI powiat bielski");
lists = lists.stream()
.map(s -> s.replaceFirst("\\s", "="))
.collect(toList());
Either if you are using an ArrayList or a HashSet you can use two ways:
Fyi: assuming your lists name is registrationList and it holds Objects names Registration
Either a for loop:
for(Registration registration : registrationList){
registration.replaceFirst(" ", "=");
}
Or you can use a Stream:
registrationList.stream.forEach(registration-> registration.replaceFirst(" ", "="));
If you have all lines in txt file and you want to modify that by replacing first space to = you could use stream API like:
List<String> collect = Files.lines(Paths.get(PATH_TO_FILE)).stream()
.map(s -> s.replaceFirst(" ", "="))
.collect(toList());
Files.write(PATH_TO_FILE, collect, StandardOpenOption.CREATE);
See more StandardOpenOption

Updating a subsection of a list with an "id" field

I am trying to learn how to use the lambda functions for sleeker code but struggling to make this work.
I have two lists. The "old" list is always shorter or the same length as the "updated list".
I want to take the objects from the "updated list" and overwrite the "stale objects" in the shorter "old list".
The lists have a unique field for each object.
For example, it is a bit like updating books in a library with new editions. The UUID (title+author) remains the same but the new object replaces the old on the shelf with a new book/object.
I know I could do it the "long way" and make a HashMap<MyUniqueFieldInMyObject, MyObject> and then take the new List<MyUpdatedObjects> and do the same.
I.e. Have HashMap<UniqueField, MyOldObject> and HashMap<UniqueField, MyUpdatedObject>, then iterate over the old objects with a pseudo "if updated objects have an entry with the same key, overwrite the value with the updated value"...
But...
Is there a "nicer" shorted way to do this with functional lambda statements?
I was thinking along the lines of:
List<MyObject> updatedList;
List<MyObject> oldList;
updatedList.forEach(MyObject -> {
String id = MyObject.getId();
if (oldList.stream().anyMatcher(MyObject ->
MyObject.getId().matches(id)) {
//Do the replacement here? If so...how?
}
}
Which is where I am lost!
Thanks for any guidance.
If you want to update the list in place rather than making a new list, you can use List.replaceAll:
oldList.replaceAll(old ->
updateListe.stream()
.filter(updated -> updated.getId().equals(old.getId())
.findFirst()
.orElse(old)
);
The main problem with this solution is that its complexity is O(size-of-old*size-of-updated). The approach you described as "long way" can protect you from having to iterate over the entire updated list for every entry in the old list:
// note that this will throw if there are multiple entries with the same id
Map<String, MyObject> updatedMap = updatedList.stream()
.collect(toMap(MyObject::getId, x->x));
oldList.replaceAll(old -> updatedMap.getOrDefault(old.getId(), old));
I recommend you to iterate over the oldList - the one you want to update. For each of the object iterated match the equivalent one by its id and replace it using Stream::map. If an object is not found, replace it with self (doesn't change the object) using Optional::orElse.
List<MyObject> newList = oldList
.stream() // Change values with map()
.map(old -> updatedList.stream() // Iterate each to find...
.filter(updated -> old.getId() == updated.getId()) // ...by the same id
.findFirst() // Get new one to replace
.orElse(old)) // Else keep the old one
.collect(Collectors.toList()); // Back to List
List<Foo> updatedList = List.of(new Foo(1L, "new name", "new desc."));
List<Foo> oldList = List.of(new Foo(1L, "old name", "old desc."));
List<Foo> collect = Stream.concat(updatedList.stream(), oldList.stream())
.collect(collectingAndThen(toMap(Foo::getId, identity(), Foo::merge),
map -> new ArrayList(map.values())));
System.out.println(collect);
This will print out:
[Foo{id=1, name='new name', details='old desc.'}]
In Foo::merge you can define which fields need update:
class Foo {
private Long id;
private String name;
private String details;
/*All args constructor*/
/*getters*/
public static Foo merge(Foo newFoo, Foo oldFoo) {
return new Foo(oldFoo.id, newFoo.name, oldFoo.details);
}
}
I think it's best to add the objects to be updated into a new list to avoid changing a list you are streaming on and then you can simply replace the old with the new list
private List<MyObject> update(List<MyObject> updatedList, List<MyObject> oldList) {
List<MyObject> newList = new ArrayList<>();
updatedList.forEach(object -> {
if (oldList.stream().anyMatch(old -> old.getUniqueId().equals(object.getUniqueId()))) {
newList.add(object);
}
}
return newList;
}

Categories

Resources