Skip to content

Commit

Permalink
Gp 155/random field comparator/completed (#137)
Browse files Browse the repository at this point in the history
* GP-155: add solution to random-field-comparator exercise
  • Loading branch information
Stanislav-Zabramnyi authored Aug 12, 2022
1 parent 51df2ed commit ee914af
Show file tree
Hide file tree
Showing 5 changed files with 371 additions and 0 deletions.
17 changes: 17 additions & 0 deletions 3-0-java-core/3-6-4-random-field-comparator/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# <img src="https://raw.githubusercontent.com/bobocode-projects/resources/master/image/logo_transparent_background.png" height=50/>Random Field Comparator
#### Improve your reflection-related skills implementing a random field comparator 💪

### Objectives
* implement a logic of choosing a random field to use it for comparison of objects of provided type ✅
* implement a mechanism to check if field type is `Comparable`
* implement a method `compare` that compares two objects by randomly-provided field ✅
* extend a method `compare` to manage null field values following condition when null value grater than a non-null value ✅
* implement method `getComparingFieldName` that retrieves the name of randomly-chosen comparing field✅
* implement method `toString`

---
#### 🆕 First time here? – [See Introduction](https://github.com/bobocode-projects/java-fundamentals-exercises/tree/main/0-0-intro#introduction)
#### ➡️ Have any feedback? – [Please fill the form ](https://forms.gle/u6kHcecFuzxV232LA)

##
<div align="center"><img src="https://raw.githubusercontent.com/bobocode-projects/resources/master/animation/GitHub%20Star_3.gif" height=50/></div>
15 changes: 15 additions & 0 deletions 3-0-java-core/3-6-4-random-field-comparator/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>3-0-java-core</artifactId>
<groupId>com.bobocode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>3-6-4-random-field-comparator</artifactId>


</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.bobocode.se;

import static java.util.Objects.requireNonNull;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import lombok.SneakyThrows;

/**
* A generic comparator that is comparing a random field of the given class. The field is either primitive or
* {@link Comparable}. It is chosen during comparator instance creation and is used for all comparisons.
* <p>
* If no field is available to compare, the constructor throws {@link IllegalArgumentException}
*
* @param <T> the type of the objects that may be compared by this comparator
*<p><p>
* <strong>TODO: to get the most out of your learning, <a href="https://www.bobocode.com/learn">visit our website</a></strong>
* <p>
*
* @author Stanislav Zabramnyi
*/
public class RandomFieldComparator<T> implements Comparator<T> {

private final Class<T> targetType;
private final Field fieldToCompare;

public RandomFieldComparator(Class<T> targetType) {
this.targetType = requireNonNull(targetType);
this.fieldToCompare = chooseFieldToCompare(targetType);
}

/**
* Compares two objects of the class T by the value of the field that was randomly chosen. It allows null values
* for the fields, and it treats null value grater than a non-null value.
*
* @param o1
* @param o2
* @return positive int in case of first parameter {@param o1} is greater than second one {@param o2},
* zero if objects are equals,
* negative int in case of first parameter {@param o1} is less than second one {@param o2}.
*/
@Override
public int compare(T o1, T o2) {
Objects.requireNonNull(o1);
Objects.requireNonNull(o2);
return compareFieldValues(o1, o2);
}

/**
* Returns the name of the randomly-chosen comparing field.
*/
public String getComparingFieldName() {
return fieldToCompare.getName();
}

/**
* Returns a statement "Random field comparator of class '%s' is comparing '%s'" where the first param is the name
* of the type T, and the second parameter is the comparing field name.
*
* @return a predefined statement
*/
@Override
public String toString() {
return String.format("Random field comparator of class '%s' is comparing '%s'", targetType.getSimpleName(),
getComparingFieldName());
}

private Field chooseFieldToCompare(Class<T> targetType) {
return Arrays.stream(targetType.getDeclaredFields())
.filter(f -> Comparable.class.isAssignableFrom(f.getType()) || f.getType().isPrimitive())
.findAny().orElseThrow(() -> new IllegalArgumentException("There are no fields available to compare"));
}

@SneakyThrows
@SuppressWarnings("unchecked")
private <U extends Comparable<? super U>> int compareFieldValues(T o1, T o2) {
fieldToCompare.setAccessible(true);
var value1 = (U) fieldToCompare.get(o1);
var value2 = (U) fieldToCompare.get(o2);
Comparator<U> comparator = Comparator.nullsLast(Comparator.naturalOrder());
return comparator.compare(value1, value2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package com.bobocode.se;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.lang.reflect.Field;
import java.util.Arrays;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(OrderAnnotation.class)
class RandomFieldComparatorTest {

private final RandomFieldComparator<Account> randomFieldComparator = new RandomFieldComparator<>(Account.class);

@Test
@Order(1)
@DisplayName("Constructor throws an exception when parameter is null")
void classDoesNotApplyNullInConstructor() {
assertThrows(NullPointerException.class, () -> new RandomFieldComparator<>(null));
}

@Test
@Order(2)
@SneakyThrows
@DisplayName("Constructor throws an exception when the target type has no Comparable fields")
void constructorThrowsExceptionIfNoComparableFieldsInProvidedType() {
assertThrows(IllegalArgumentException.class, () -> new RandomFieldComparator<>(ClassWithNotComparableField.class));
}

@Test
@Order(3)
@DisplayName("Method 'compare' throws an exception when any parameter is null")
void compareWhenFirstParameterAreNull() {

assertThrows(NullPointerException.class, () -> randomFieldComparator.compare(null, new Account()));
assertThrows(NullPointerException.class, () -> randomFieldComparator.compare(new Account(), null));
}

@Test
@Order(4)
@DisplayName("Method 'compare' returns 0 when field values of both objects are null")
void compareWhenBothFieldValuesIsNull() {
setFieldToCompare("lastName", Account.class);
int compareResult = randomFieldComparator.compare(new Account(), new Account());

assertThat(compareResult).isZero();
}

@Test
@Order(5)
@DisplayName("Method compare returns positive int when the first field value is null")
void compareWhenFieldValuesOfFirstObjectIsNull() {
Account emptyAccount = new Account();
Account account = new Account("Sibma", "LoinKing", "[email protected]", 14);
setFieldToCompare("email", Account.class);//set field to compare explicitly as there are int field which has default value 0
int compareResult = randomFieldComparator.compare(emptyAccount, account);

assertThat(compareResult).isPositive();
}

@Test
@Order(6)
@DisplayName("Method compare returns negative int when the second field value is null")
void compareWhenFieldValuesOfSecondObjectIsNull() {
Account account = new Account("Mufasa", "LoinKing", "[email protected]", 47);
Account emptyAccount = new Account();
setFieldToCompare("firstName", Account.class);
int compareResult = randomFieldComparator.compare(account, emptyAccount);

assertThat(compareResult).isNegative();
}

@Test
@Order(7)
@SneakyThrows
@DisplayName("Method 'compare' returns positive int when the first value is greater")
void compareWhenFieldValueOfFirstObjectIsGrater() {
var fieldToCompareName = "firstName";
Account account1 = new Account();
Account account2 = new Account();
Field fieldToCompareAccount = account1.getClass().getDeclaredField(fieldToCompareName);
fieldToCompareAccount.setAccessible(true);

fieldToCompareAccount.set(account1, "Bob");
fieldToCompareAccount.set(account2, "Alice");

setFieldToCompare(fieldToCompareName, Account.class);
int compareResult = randomFieldComparator.compare(account1, account2);

assertThat(compareResult).isPositive();
}

@Test
@Order(8)
@SneakyThrows
@DisplayName("Method 'compare' returns negative int when the first value is smaller")
void compareWhenFieldValueOfSecondObjectIsGrater() {
var fieldToCompareName = "firstName";
Account account1 = new Account();
Account account2 = new Account();
Field fieldToCompareAccount = account1.getClass().getDeclaredField(fieldToCompareName);
fieldToCompareAccount.setAccessible(true);

fieldToCompareAccount.set(account1, "Alice");
fieldToCompareAccount.set(account2, "Bob");

setFieldToCompare(fieldToCompareName, Account.class);
int compareResult = randomFieldComparator.compare(account1, account2);

assertThat(compareResult).isNegative();
}

@Test
@Order(9)
@SneakyThrows
@DisplayName("Method 'compare' returns zero when the field values are equal")
void compareWhenFieldValuesOfObjectsAreEqual() {
var fieldToCompareName = "firstName";
Account account1 = new Account();
Account account2 = new Account();
Field fieldToCompareAccount = account1.getClass().getDeclaredField(fieldToCompareName);
fieldToCompareAccount.setAccessible(true);

fieldToCompareAccount.set(account1, "Carol");
fieldToCompareAccount.set(account2, "Carol");

setFieldToCompare(fieldToCompareName, Account.class);
int compareResult = randomFieldComparator.compare(account1, account2);

assertThat(compareResult).isZero();
}

@Test
@Order(10)
@SneakyThrows
@DisplayName("Method 'compare' returns positive int when the first primitive value is greater")
void comparePrimitivesWhenFieldValueOfFirstObjectIsGrater() {
var fieldToCompareName = "age";
Account account1 = new Account();
Account account2 = new Account();
Field fieldToCompareAccount = account1.getClass().getDeclaredField(fieldToCompareName);
fieldToCompareAccount.setAccessible(true);

fieldToCompareAccount.setInt(account1, 7);
fieldToCompareAccount.setInt(account2, 3);

setFieldToCompare(fieldToCompareName, Account.class);
int compareResult = randomFieldComparator.compare(account1, account2);

assertThat(compareResult).isPositive();
}

@Test
@Order(11)
@SneakyThrows
@DisplayName("Method 'compare' returns zero when the primitive field values are equal")
void comparePrimitivesWhenFieldValuesOfObjectsAreEqual() {
var fieldToCompareName = "age";
Account account1 = new Account();
Account account2 = new Account();
Field fieldToCompareAccount = account1.getClass().getDeclaredField(fieldToCompareName);
fieldToCompareAccount.setAccessible(true);

fieldToCompareAccount.setInt(account1, 15);
fieldToCompareAccount.setInt(account2, 15);

setFieldToCompare(fieldToCompareName, Account.class);
int compareResult = randomFieldComparator.compare(account1, account2);

assertThat(compareResult).isZero();
}

@Test
@Order(12)
@SneakyThrows
@DisplayName("Method 'compare' returns negative int when the first primitive value is smaller")
void comparePrimitivesWhenFieldValueOfSecondObjectIsGrater() {
var fieldToCompareName = "age";
Account account1 = new Account();
Account account2 = new Account();
Field fieldToCompareAccount = account1.getClass().getDeclaredField(fieldToCompareName);
fieldToCompareAccount.setAccessible(true);

fieldToCompareAccount.setInt(account1, 4);
fieldToCompareAccount.setInt(account2, 8);

setFieldToCompare(fieldToCompareName, Account.class);
int compareResult = randomFieldComparator.compare(account1, account2);

assertThat(compareResult).isNegative();
}

@Test
@Order(13)
@SneakyThrows
@DisplayName("Method 'getComparingFieldName' returns the name of randomly-chosen field to be compared")
void getComparingFieldName() {
var fieldToCompareName = "lastName";
setFieldToCompare(fieldToCompareName, Account.class);

assertEquals(fieldToCompareName, randomFieldComparator.getComparingFieldName());
}

@Test
@Order(14)
@SneakyThrows
@DisplayName("Method toString is properly overridden")
void toStringOverriding() {
var expectedString = "Random field comparator of class 'Account' is comparing 'email'";
var fieldToCompareName = "email";
setFieldToCompare(fieldToCompareName, Account.class);

assertEquals(expectedString, randomFieldComparator.toString());
}

@SneakyThrows
private <T> void setFieldToCompare(String fieldName, Class<T> classType) {
Field fieldToCompare = Arrays.stream(randomFieldComparator.getClass().getDeclaredFields())
.filter(f -> f.getType().equals(Field.class))
.findAny()
.orElseThrow();
fieldToCompare.setAccessible(true);
fieldToCompare.set(randomFieldComparator, classType.getDeclaredField(fieldName));
}

private static class EmptyClass {

}

@AllArgsConstructor
private static class ClassWithNotComparableField {

private Object field;
}

@NoArgsConstructor
@AllArgsConstructor
private static class Account {

private String firstName;
private String lastName;
private String email;
private int age;
}
}
1 change: 1 addition & 0 deletions 3-0-java-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<module>3-6-1-file-reader</module>
<module>3-6-2-file-stats</module>
<module>3-6-3-crazy-regex</module>
<module>3-6-4-random-field-comparator</module>
</modules>


Expand Down

0 comments on commit ee914af

Please sign in to comment.