-
-
Notifications
You must be signed in to change notification settings - Fork 464
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Gp 155/random field comparator/completed #137
Changes from 4 commits
f21dd68
4b69006
41f167a
0c522db
65f705e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> |
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,82 @@ | ||
package com.bobocode.se; | ||
|
||
import com.bobocode.util.ExerciseNotCompletedException; | ||
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) { | ||
Objects.requireNonNull(targetType); | ||
this.targetType = 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}. | ||
*/ | ||
@SneakyThrows | ||
@SuppressWarnings("unchecked") | ||
@Override | ||
public int compare(T o1, T o2) { | ||
Objects.requireNonNull(o1); | ||
Objects.requireNonNull(o2); | ||
fieldToCompare.setAccessible(true); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You user @Override
public int compare(T o1, T o2) {
Objects.requireNonNull(o1);
Objects.requireNonNull(o2);
return compareFieldValues(o1, o2);
}
@SneakyThrows
@SuppressWarnings("unchecked")
private <U extends Comparable<? super U>> int compareFieldValues(T o1, T o2) {
field.setAccessible(true);
var value1 = (U) field.get(o1);
var value2 = (U) field.get(o2);
Comparator<U> comparator = Comparator.nullsLast(Comparator.naturalOrder());
return comparator.compare(value1, value2);
} What I like about helper method is that you can put |
||
var field1 = (Comparable) fieldToCompare.get(o1); | ||
var field2 = (Comparable) fieldToCompare.get(o2); | ||
|
||
return Comparator.<Comparable>nullsLast(Comparator.naturalOrder()).compare(field1, field2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd move comparator into a local variable to make this line cleaner. |
||
} | ||
|
||
/** | ||
* 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")); | ||
} | ||
} |
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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes sense to do
this.targetType = Objects.requireNonNull(targetType);
or eventhis.targetType = requireNonNull(targetType);