Java | Posing Boolean expressions on hashMap entries - java

I have a hashMap that contains "term" as key and "documents list" as values.
Eg:
KEY::VALUE
afternoon::Doc2
activities::Doc1, Doc2, Doc3
admissions::Doc1, Doc2, Doc4, Doc5
alternate::Doc5
I need to pass boolean expressions against the terms and fetch the matching documents. This expression will be passed through another string variable.
Eg:
(afternoon AND activities) OR alternate => Doc2, Doc5
(afternoon AND activities) OR (admissions AND alternate) => Doc2, Doc5
activities AND NOT afternoon = > Doc1, Doc3
Are there any functions in Java for such operations? Any external libraries will work too.
A code snippet will help me a lot since my assignment is due tomorrow & this is the final step of my solution.

Iterate through the map values and find all the keys corresponding to that value, to make a Map<String, Set<String>> where the key is eg. Doc1 and the value is eg. activities,admissions. Represent each expression as a Predicate<Set<String>>, which looks at a set of terms and decides whether it matches the expression.
Now,
map.entrySet().stream().filter(e -> expression.test(e.getValue())).map(Entry::getKey).collect(Collectors.toList());
returns the list of documents matching a term expression

The way I see it, you want to achieve union and intersection of list elements for boolean expressions.
You can write a custom method like
public Set<Doc> andOp(Set<Doc> list1, Set<String> Doc) {
if(list1.isEmpty() || list2.isEmpty())
return new HashSet<>();
Set<Doc> result = new HashSet<>();
Iterator<Doc> it = list1.iterator();
while( it.hasNext()) {
Doc Doc1 = it.next();
if(list2.contains(Doc))
result.add(Doc);
}
return result;
}
public Set<Doc> orOp(Set<Doc> list1, Set<Doc> list2) {
if(list1.isEmpty()) return list2;
if(list2.isEmpty()) return list1;
Set<Doc> result = new HashSet<>();
result.addAll(list1);
result.addAll(list2);
return result;
}
So, the resultant expression for (afternoon AND activities) OR (admissions AND admissions) would be
Set<Doc> doc1 = andOp(map.get("activities"), map.get("afternoon"));
Set<Doc> doc2 = andOp(map.get("admissions"), map.get("admissions"));
doc1 = orOp(doc1, doc2);

Related

Java multikey map with no value

I would like to be able to query on each key with no value.
A1...n , B1...n are Strings.
I have Sets of Strings which I need to add into structure in order to be able to query on each String and get its group Strings.
For example : {A1, A2, A3} , {B1,B2,B3, B4....., Bn}
map.get(A1) --> return {A2,A3}
map.get(A2) --> return {A1,A3}
map.get(A3) --> return {A1,A2}
map.get(B1) --> return {B2,B3, B4...Bn}
map.get(B2) --> return {B1,B3, B4 ..Bn}
map.get(B3) --> return {B1,B2, B4...Bn}
etc...
Any recommendations which data structure should I use?
I suggest you make a map that maps an individual key to its entire group.
So, instead of what you wrote, this:
A1 -> {A1, A2, A3}
If you then have an operation such as 'list members of the group, but dont list yourself', just write that logic at that point (loop through the group and skip over yourself / use a stream().filter() operation to do this).
This way you save a ton of memory - each 'group' can simply be the same object. This also means that if you have mutable groups, you can just operate on the group (though, you'd have to take good care to update the map in tandem):
String[][] input = {
{"A1", "A2", "A3"},
{"B1", "B2"}};
public Map<String, SortedSet<String>> makeGroupsData() {
var out = new HashMap<String, List<String>>();
for (String[] group : input) {
SortedSet<String> groupSet = new TreeSet<>(Arrays.asList(group));
for (String g : group) out.put(g, groupSet);
}
return out;
}
// and then for operations:
/** Returns a list of all <em>other</em> members of the group */
public SortedSet<String> getGroupMembers(String key) {
var group = groups.get(key);
if (group == null) throw new IllegalArgumentException("unknown: " + key);
var out = new TreeSet<String>(group);
out.remove(key);
return out;
}
public int getGroupSize(String key) {
Set<String> group = groups.get(key);
return group == null ? 0 : group.size();
}
You get the drift - I'm not sure if SortedSet is right for you here, for example. The point is, this means there is only 1 object (one TreeSet) for one group, and the map just links each individual member to the same one group object.
NB: Big caveat: This assumes any given string cannot be a member of more than one group, and this code does not check for it. You may want to (the .put method returns the previous mapping, so all you have to do is check if the .put method returns a non-null value; if it does, throw an IllegalArgumentException).

How to search multiple field in List using Java

I have a List of following objects:
public class OptionDetailResponse {
private long id;
private String flavor;
private String size;
private String status;
private String barcode;
}
I want to search in a List of those objects based on all 4 fields (except id):
flavor (input from a combobox)
size (input from a combobox)
status (input from a combobox)
barcode (input from a textfield)
This is my UI with the 4 input fields:
What I tried
I tried to use Predicate<OptionDetailResponse> for searching:
Predicate<OptionDetailResponse> selectFlavor = e -> e.getParentName().equals(flavor);
Predicate<OptionDetailResponse> selectSize = e -> e.getName().equals(size);
Predicate<OptionDetailResponse> selectStatus = e -> e.getStatus().equals(status);
Predicate<OptionDetailResponse> inputBarcode = e -> e.getBarcode().contains(barcode);
List<OptionDetailResponse> list = responseList.stream().filter(
selectFlavor.and(selectSize).and(selectStatus).and(inputBarcode))
.collect(Collectors.<OptionDetailResponse>toList());
But the list returned only a correct result when selected a value for in all search-fields.
Questions
How can I have all list when all field is empty using Predicate ?
Do have other ways to search by multiple fields ?
I think you can check on nullability or on specific value which shouldn't be checked inside each of your predicates depending on value you have in unselected field. I think it can look like this:
Predicate<OptionDetailResponse> selectFlavor = e -> flavor == null || e.getParentName().equals(flavor);
or
Predicate<OptionDetailResponse> selectFlavor = e -> flavor.equals("your unselected flavor value") || e.getParentName().equals(flavor);
.. and same for other predicates.
Bear in mind that when you use a Predicate in a filter method, the result will be the list of elements which match the "test" operation of the supplied predicate.
What you have done is to create a chain of Predicates in logical AND. This means that the result of the filter will be the list of the elements which match ALL given predicates.
if you need for a different result, you can create your chain by applying by using the specific Predicate functions, and thus realize eventually a more complex condition.
Other than and(Predicate<> target), for example you have the following method:
or(Predicate<> target): short-circuiting logical OR between two Predicate
negate(): logical negation of the current instance of Predicate
not(Predicate<> target): returns a predicate that is the negation of the supplied predicate
Probably you want to use following filter on each respective field:
if a search-parameter not supplied by UI (or left empty), then don't apply predicate: means predicate matches all regardless of object's field value
if a search-parameter is supplied by UI (or not empty), then apply the predicate
That said you could use a filter combined from all filled input-fields in stream:
// renamed your stream-result variable to indicate that it was filtered
List<OptionDetailResponse> filteredResult = responseList.stream()
.filter( buildPredicateFromInputFields(flavor, size, status, barcode) )
.collect(Collectors.toList());
where the predicate passed as argument to filter is combined from the 4 fields:
// you could name the method more specific: matchesAllNonEmptyInputs
Predicate<OptionDetailResponse> buildPredicateFromInputFields(
String flavor,
String size,
String status,
String barcode
) {
// build a set of field-matchers (predicate) based on given input fields
// null or empty (or blank) fields are excluded from the set
var givenFieldPredicates = new ArrayList<Predicate<OptionDetailResponse>>(4); // max 4 entries
if (flavor != null && !flavor.isBlank()) {
givenFieldPredicates.add(obj -> flavor.equals(obj.flavor))
}
if (size != null && !size.isBlank()) {
givenFieldPredicates.add(obj -> size.equals(obj.size))
}
if (status != null && !status.isBlank()) {
givenFieldPredicates.add(obj -> status.equals(obj.size))
}
// contained (partial match allowed)
if (barcode != null && !barcode.isBlank()) {
// will throw NullPointerException if object has null barcode!
givenFieldPredicates.add(obj -> obj.barcode.contains(barcode))
}
// combined them using AND: each field predicate must match
return givenFieldPredicates.stream().reduce(x -> true, Predicate::and);
}
See also:
Baeldung's Java Tutorial: Java 8 Predicate Chain, section "6. Combining a Collection of Predicates"
We could use Function, BiFunction and method reference and a pojo to hold the way to filter a field list to build something like
#Value
public static class Filter<T> {
private Function<OptionDetailResponse, T> getter;
private BiFunction<T, T, Boolean> filter;
public Predicate<OptionDetailResponse> toPredicate(OptionDetailResponse criteria) {
return o -> filter.apply(getter.apply(o), getter.apply(criteria));
}
}
public static List<Filter<?>> filters() {
List<Filter<?>> filterList = new ArrayList<>();
filterList.add(new Filter<>(OptionDetailResponse::getFlavor, Object::equals));
filterList.add(new Filter<>(OptionDetailResponse::getSize, Object::equals));
filterList.add(new Filter<>(OptionDetailResponse::getStatus, Object::equals));
filterList.add(new Filter<>(OptionDetailResponse::getBarcode, String::contains));
return filterList;
}
public static final List<Filter<?>> FILTERS = filters();
public Predicate<OptionDetailResponse> buildPredicate(OptionDetailResponse searchCriteria) {
return FILTERS
.stream()
.filter(f -> f.getGetter().apply(searchCriteria) != null)
.map(f -> f.toPredicate(searchCriteria))
.reduce(o -> true, Predicate::and);
}
public List<OptionDetailResponse> search(List<OptionDetailResponse> responseList,
OptionDetailResponse searchCriteria) {
return responseList.stream()
.filter(buildPredicate(searchCriteria))
.collect(Collectors.toList());
}

Using Java 8 Streams to accomplish removing loops

I have a list of documents objects that need to be mapped based on certain criteria. There is a utility function that takes any 2 document types and determines if they match on a number of criteria, like genre of document, whether they share any authors etc. The code works but I';d like to use Java Streams to solve it if possible.
I currently solve this by using the following code:
class Document{
private String genre;
private List<Author> authors;
private String isbn;
private boolean paperBack;
...
}
I also use a library utility that has a function that returns true given a series of matching criteria and a pair of documents. It simply returns a boolean.
boolean matchesOnCriteria(Document doc1, Document doc2, Criteria criteria){
...
}
Here is the matching code for finding the books that match on the provided criteria
DocumentUtils utils = DocumentUitls.instance();
Criteria userCriteria = ...
List<Pair> matches = new ArrayList<>();
List<Document> documents = entityManager.findBy(.....);
for(Document doc1 : documents){
for(Documents doc2 : documents){
if(!doc1.equals(doc2){
if (utils.matchesOnCriteria(doc1,doc2, userCriteria)) {
Pair<Document> p = new Pair(doc1,doc2);
if(!matches.contains(p)){
matches.add(p);
}
}
}
}
}
}
How can I do this using Streams?
The idea of the following solution using Steam::reduce is simple:
Group the qualified pairs of documents to Map<Document, List<Document>> having all possible acceptable combinations. Let's say odd and even documents are in pairs:
D1=[D3, D5], D2=[D4], D3=[D1, D5], D4=[D2], D5[D1, D3] // dont mind the duplicates
Using Stream::reduce you can achieve the following steps:
Transform entries to Pair<>,
D1-D3, D1-D5, D2-D4, D3-D1, D1-D5, D4-D2, D5-D1, D5-D3
Save these items to Set guaranteeing the equal pairs occur once (D1-D3 = D3-D1). The condition the Pair must override both Object::equals and Object:hashCode and implements equality based on the both documents present.
D1-D3, D1-D5, D3-D5, D2-D4
Reducing (merging) the particular sets into a single collection Set<Pair<Document>>.
Map<Document, List<Document>> map = documents.stream()
.collect(Collectors.toMap( // Collected to Map<Document, List<Document>>
Function.identity(), // Document is the key
d1 -> documents.stream() // Value are the qualified documents
.filter(d2 -> !d1.equals(d2) &&
utils.matchesOnCriteria(d1,d2, userCriteria)
.collect(Collectors.toList()))); // ... as List<Document>
Set<Pair<Document>> matches = map.entrySet().stream().reduce( // Reduce the Entry<Dokument, List<Document>>
new HashSet<>(), // ... to Set<Pair<>>
(set, e) -> {
set.addAll(e.getValue().stream() // ... where is
.map(v -> new Pair<Document>(e.getKey(), v)) // ... the Pair of qualified documents
.collect(Collectors.toSet()));
return set;
},
(left, right) -> { left.addAll(right); return left; }); // Merge operation
The condition !matches.contains(p) is redundant, there are better ways to assure distinct values. Either use Stream::distinct or collect the stream to Set which is an unordered distinct collection.
Read more at Baeldung's: remove all duplicates.

How can I find Documents with Duplicate Array Elements?

Here is my Document:
{
"_id":"5b1ff7c53e3ac841302cfbc2",
"idProf":"5b1ff7c53e3ac841302cfbbf",
"pacientes":["5b20d2c83e3ac841302cfbdb","5b20d25f3e3ac841302cfbd0"]
}
I want to know how to find a duplicate entry in the array using MongoCollection in Java.
This is what I'm trying:
BasicDBObject query = new BasicDBObject("idProf", idProf);
query.append("$in", new BasicDBObject().append("pacientes", idJugador.toString()));
collection.find(query)
We can try to solve this in your Java-application code.
private final MongoCollection collection;
public boolean hasDuplicatePacientes(String idProf) {
Document d = collection.find(eq("idProf", idProf)).first();
List<String> pacientes = (List<String>) d.get("pacientes");
int original = pacientes.size();
if (original == 0) {
return false;
}
Set<String> unique = new HashSet(pacientes);
return original != unique.size();
}
Or if you're searching for a way to do this fully on db-side, I believe it's also possible with something like Neil Lunn provided.
The best approach really is to compare the length of the array to the length of an array which would have all duplicates removed. A "Set" does not have duplicate entries, so what you need to do is convert an array into a "Set" and test against the original.
Modern MongoDB $expr
Modern MongoDB releases have $expr which can be used with aggregation expressions in a regular query. Here the expressions we would use are $setDifference and $size along with $ne for the boolean comparison:
Document query = new Document(
"$expr", new Document(
"$ne", Arrays.asList(
new Document("$size", "$pacientes"),
new Document("$size",
new Document("$setDifference", Arrays.asList("$pacientes", Collections.emptyList()))
)
)
)
);
MongoCursor<Document> cursor = collection.find(query).iterator();
Which serializes as:
{
"$expr": {
"$ne": [
{ "$size": "$pacientes" },
{ "$size": { "$setDifference": [ "$pacientes", [] ] } }
]
}
}
Here it is actually the $setDifference which is doing the comparison and returning only unique elements. The $size is returning the length, both of the original document array content and the newly reduced "set". And of course where these are "not equal" ( the $ne ) the condition would be true meaning that a duplicate was found in the document.
The $expr operates on receiving a boolean true/false value in order whether to consider the document a match for the condition or not.
Earlier Version $where clause
Basically $where is a JavaScript expression that evaluates on the server
String whereClause = "this.pacientes.length != Object.keys(this.pacientes.reduce((o,e) => Object.assign(o, { [e.valueOf()]: null}), {})).length";
Document query = new Document("$where": whereClause);
MongoCursor<Document> cursor = collection.find(query).iterator();
You do need to have not explicitly disabled JavaScript evaluation on the server ( which is the default ) and it's not as efficient as using $expr and the native aggregation operators. But JavaScript expressions can be evaluated in the same way using $where, and the argument in Java code is basically sent as a string.
In the expression the .length is a property of all JavaScript arrays, so you have the original document content and the comparison to the "set". The Array.reduce() uses each array element as a "key" in a resulting object, from which the Object.keys() will then return those "keys" as a new array.
Since JavaScript objects work like a Map, only unique keys are allowed and this is a way to get that "set" result. And of course the same != comparison will return true when the removal of duplicate entries resulted in a change of length.
In either case of $expr or $where these are computed conditions which cannot use an index where present on the collection. As such it is generally recommended that additional criteria which use regular equality or range based query expressions which can indeed utilize an index be used alongside these expressions. Such additional criteria in the predicate would improve query performance greatly where an index is in place.

Remake list with some condition

There are two entities:
class GiftCertificate {
Long id;
List<Tag> tags;
}
class Tag {
Long id;
String name;
}
There is a list
List<GiftCertificate>
which contains, for example, the following data:
<1, [1, "Tag1"]>, <2, null>, <1, [2, "Tag2"]>. (It does not contain a set of tags, but only one tag or does not have it at all).
I need to do so that in the result it was this:
<1, {[1," Tag1 "], [2," Tag2 "]}>, <2, null>. I mean, add to the set of the first object a tag from the third GiftCertificate and at the same time delete the 3rd one. I would like to get at least some ideas on how to do this. it would be nice to use stream.
Probably not the most effective way, but it might help
private List<GiftCertificate> joinCertificates(List<GiftCertificate> giftCertificates) {
return giftCertificates.stream()
.collect(Collectors.groupingBy(GiftCertificate::getId))
.entrySet().stream()
.map(entry -> new GiftCertificate(entry.getKey(), joinTags(entry.getValue()))).collect(Collectors.toList());
}
private List<Tag> joinTags(List<GiftCertificate> giftCertificates) {
return giftCertificates.stream()
.flatMap(giftCertificate -> Optional.ofNullable(giftCertificate.getTags()).stream().flatMap(Collection::stream))
.collect(Collectors.toList());
}
You can do what you want with streams and with the help of a dedicated custom constructor and a couple of helper methods in GiftCertificate. Here's the constructor:
public GiftCertificate(GiftCertificate another) {
this.id = another.id;
this.tags = new ArrayList<>(another.tags);
}
This just works as a copy constructor. We're creating a new list of tags, so that if the list of tags of either one of the GiftCertificate instances is modified, the other one won't. (This is just basic OO concepts: encapsulation).
Then, in order to add another GiftCertificate's tags to this GiftCertificate's list of tags, you could add the following method to GiftCertificate:
public GiftCertificate addTagsFrom(GiftCertificate another) {
tags.addAll(another.tags);
return this;
}
And also, a helper method that returns whether the list of tags is empty or not will come in very handy:
public boolean hasTags() {
return tags != null && !tags.isEmpty();
}
Finally, with these three simple methods in place, we're ready to use all the power of streams to solve the problem in an elegant way:
Collection<GiftCertificate> result = certificates.stream()
.filter(GiftCertificate::hasTags) // keep only gift certificates with tags
.collect(Collectors.toMap(
GiftCertificate::getId, // group by id
GiftCertificate::new, // use our dedicated constructor
GiftCertificate::addTagsFrom)) // merge the tags here
.values();
This uses Collectors.toMap to create a map that groups gift certificates by id, merging the tags. Then, we keep the values of the map.
Here's the equivalent solution, without streams:
Map<Long, GiftCertificate> map = new LinkedHashMap<>(); // preserves insertion order
certificates.forEach(cert -> {
if (cert.hasTags()) {
map.merge(
cert.getId(),
new GiftCertificate(cert),
GiftCertificate::addTagsFrom);
}
});
Collection<GiftCertificate> result = map.values();
And here's a variant with a slight performance improvement:
Map<Long, GiftCertificate> map = new LinkedHashMap<>(); // preserves insertion order
certificates.forEach(cert -> {
if (cert.hasTags()) {
map.computeIfAbsent(
cert.getId(),
k -> new GiftCertificate(k)) // or GitCertificate::new
.addTagsFrom(cert);
}
});
Collection<GiftCertificate> result = map.values();
This solution requires the following constructor:
public GiftCertificate(Long id) {
this.id = id;
this.tags = new ArrayList<>();
}
The advantage of this approach is that new GiftCertificate instances will be created only if there's no other entry in the map with the same id.
Java 9 introduced flatMapping collector that is particularly well-suited for problems like this. Break the task into two steps. First, build a map of gift certificate IDs to list of tags and then assemble a new list of GiftCertificate objects:
import static java.util.stream.Collectors.flatMapping;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
......
Map<Long, List<Tag>> gcIdToTags = gcs.stream()
.collect(groupingBy(
GiftCertificate::getId,
flatMapping(
gc -> gc.getTags() == null ? Stream.empty() : gc.getTags().stream(),
toList()
)
));
List<GiftCertificate> r = gcIdToTags.entrySet().stream()
.map(e -> new GiftCertificate(e.getKey(), e.getValue()))
.collect(toList());
This assumes that GiftCertificate has a constructor that accepts Long id and List<Tag> tags
Note that this code deviates from your requirements by creating an empty list instead of null in case there are no tags for a gift certificate id. Using null instead of an empty list is just a very lousy design and forces you to pollute your code with null checks everywhere.
The first argument to flatMapping can also be written as gc -> Stream.ofNullable(gc.getTags()).flatMap(List::stream) if you find that more readable.

Categories