diff --git a/3-0-java-core/3-6-4-random-field-comparator/README.MD b/3-0-java-core/3-6-4-random-field-comparator/README.MD new file mode 100644 index 000000000..c361e6aa4 --- /dev/null +++ b/3-0-java-core/3-6-4-random-field-comparator/README.MD @@ -0,0 +1,17 @@ +# 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) + +## +
\ No newline at end of file diff --git a/3-0-java-core/3-6-4-random-field-comparator/pom.xml b/3-0-java-core/3-6-4-random-field-comparator/pom.xml new file mode 100644 index 000000000..557dc1f36 --- /dev/null +++ b/3-0-java-core/3-6-4-random-field-comparator/pom.xml @@ -0,0 +1,15 @@ + + + + 3-0-java-core + com.bobocode + 1.0-SNAPSHOT + + 4.0.0 + + 3-6-4-random-field-comparator + + + \ No newline at end of file diff --git a/3-0-java-core/3-6-4-random-field-comparator/src/main/java/com/bobocode/se/RandomFieldComparator.java b/3-0-java-core/3-6-4-random-field-comparator/src/main/java/com/bobocode/se/RandomFieldComparator.java new file mode 100644 index 000000000..ee33f4159 --- /dev/null +++ b/3-0-java-core/3-6-4-random-field-comparator/src/main/java/com/bobocode/se/RandomFieldComparator.java @@ -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. + *

+ * If no field is available to compare, the constructor throws {@link IllegalArgumentException} + * + * @param the type of the objects that may be compared by this comparator + *

+ * TODO: to get the most out of your learning, visit our website + *

+ * + * @author Stanislav Zabramnyi + */ +public class RandomFieldComparator implements Comparator { + + private final Class targetType; + private final Field fieldToCompare; + + public RandomFieldComparator(Class 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 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 > int compareFieldValues(T o1, T o2) { + fieldToCompare.setAccessible(true); + var value1 = (U) fieldToCompare.get(o1); + var value2 = (U) fieldToCompare.get(o2); + Comparator comparator = Comparator.nullsLast(Comparator.naturalOrder()); + return comparator.compare(value1, value2); + } +} diff --git a/3-0-java-core/3-6-4-random-field-comparator/src/test/java/com/bobocode/se/RandomFieldComparatorTest.java b/3-0-java-core/3-6-4-random-field-comparator/src/test/java/com/bobocode/se/RandomFieldComparatorTest.java new file mode 100644 index 000000000..1733568e5 --- /dev/null +++ b/3-0-java-core/3-6-4-random-field-comparator/src/test/java/com/bobocode/se/RandomFieldComparatorTest.java @@ -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 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", "simba-bimba@gmail.com", 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", "simba-bimba@gmail.com", 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 void setFieldToCompare(String fieldName, Class 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; + } +} \ No newline at end of file diff --git a/3-0-java-core/pom.xml b/3-0-java-core/pom.xml index 792d82a8b..917e0cf17 100644 --- a/3-0-java-core/pom.xml +++ b/3-0-java-core/pom.xml @@ -14,6 +14,7 @@ 3-6-1-file-reader 3-6-2-file-stats 3-6-3-crazy-regex + 3-6-4-random-field-comparator