From b96ca41ec382ae9a09be3bd19d63e40345fe10dd Mon Sep 17 00:00:00 2001 From: Serhii Bondarenko Date: Thu, 15 Dec 2022 16:54:22 +0200 Subject: [PATCH 1/2] [GP-165] Implemented tests for RB-tree --- .../2-2-7-red-black-tree/README.md | 17 + .../2-2-7-red-black-tree/pom.xml | 15 + .../com/bobocode/cs/BinarySearchTree.java | 32 + .../bobocode/cs/RedBlackBinarySearchTree.java | 74 +++ .../cs/RedBlackBinarySearchTreeTest.java | 572 ++++++++++++++++++ 2-0-data-structures-and-algorithms/pom.xml | 1 + 6 files changed, 711 insertions(+) create mode 100644 2-0-data-structures-and-algorithms/2-2-7-red-black-tree/README.md create mode 100644 2-0-data-structures-and-algorithms/2-2-7-red-black-tree/pom.xml create mode 100644 2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/BinarySearchTree.java create mode 100644 2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java create mode 100644 2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/README.md b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/README.md new file mode 100644 index 000000000..d6c5c9425 --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/README.md @@ -0,0 +1,17 @@ +# Reb-Black Tree +Learn a Red-Black tree data structure and build strong skills creating balanced tree implementation 💪 + +### Objectives (at the end of the training you will be able to...) +* understand the **main idea** behind the **Red-Black Tree** data structure ✅ +* get knowledge in one of the tree balancing strategies +* implement rotations and recoloring algorithms +* catch what is different from simple BinarySearchTree and why it's better +* get stronger knowledge in algorithms field + +* The more about Red Black Tree can be found on [Wiki page](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) +--- +#### 🆕 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/7U9XZHuTtT5xpjXR6) + +## +
\ No newline at end of file diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/pom.xml b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/pom.xml new file mode 100644 index 000000000..5b59da3b3 --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/pom.xml @@ -0,0 +1,15 @@ + + + + 2-0-data-structures-and-algorithms + com.bobocode + 1.0-SNAPSHOT + + 4.0.0 + + 2-2-7-red-black-tree + + + \ No newline at end of file diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/BinarySearchTree.java b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/BinarySearchTree.java new file mode 100644 index 000000000..d7462484d --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/BinarySearchTree.java @@ -0,0 +1,32 @@ +package com.bobocode.cs; + +import java.util.function.Consumer; + +public interface BinarySearchTree> { + /** + * insert an element + * @return true if element did not exist in the tree and was inserted successfully + */ + boolean insert(T element); + + /** + * @return true if tree contains element + */ + boolean contains(T element); + + /** + * @return number of elements in the tree + */ + int size(); + + /** + * @return max. number of transition between root node and any other node; 0 - if tree is empty or contains 1 element + */ + int depth(); + + /** + * traverse the tree in element's natural order + * @param consumer accepts ref. to node during traversing + */ + void inOrderTraversal(Consumer consumer); +} diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java new file mode 100644 index 000000000..5f1425387 --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java @@ -0,0 +1,74 @@ +package com.bobocode.cs; + +import java.util.function.Consumer; + +import com.bobocode.util.ExerciseNotCompletedException; + +/** + * {@link RedBlackBinarySearchTree} is an implementation of a {@link BinarySearchTree} that is based on nodes, + * coloring, rotations and recursion. A tree node is represented as a nested class {@link Node}. It holds an element (a value), + * references to the parent node, left/right child nodes and also a field type of {@link Color} which is Enum with two possible values RED and BLACK. + * The BinarySearch tree considered as a RedBlackTree when it meets next properties: + * 1. Every {@link Node} is either RED or BLACK {@link Color}. + * 2. RED node can't have RED children nodes + * 3. The root is always BLACK. + * 4. For each node, all simple paths from the node to descendant leaves(null nodes) contain the same number of BLACK nodes + * 5. Every leaf(null node) is null and should be treated as a BLACK node + * + * To fulfill these requirements the tree need additional steps after insertion of new node to re-balance itself using + * recoloring of nodes and rotations. Every newly inserted node should be RED before re-balancing. + * + * Red-Black Tree example: + * 7(B) + * / \ + * / \ + * 3(B) 10(R) + * / \ / \ + * / \ / \ + * 2(R) null 9(B) 12(B) + * / \ / \ / \ + * / \ / \ / \ + * null null null null null 33(R) + * + * + *

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

+ * + * @param a type of elements that are stored in the tree + * @author Serhii Bondarenko + * @author Taras Boychuk + */ + +public class RedBlackBinarySearchTree> implements BinarySearchTree { + + public static > RedBlackBinarySearchTree of(T... elements) { + throw new ExerciseNotCompletedException(); + } + + @Override + public boolean insert(T element) { + throw new ExerciseNotCompletedException(); + } + + @Override + public boolean contains(T element) { + throw new ExerciseNotCompletedException(); + } + + @Override + public int size() { + throw new ExerciseNotCompletedException(); + } + + @Override + public int depth() { + throw new ExerciseNotCompletedException(); + } + + @Override + public void inOrderTraversal(Consumer consumer) { + throw new ExerciseNotCompletedException(); + } + +} diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java new file mode 100644 index 000000000..ac18f5bed --- /dev/null +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java @@ -0,0 +1,572 @@ +package com.bobocode.cs; + +import static java.lang.reflect.Modifier.isStatic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.apache.commons.lang3.tuple.Pair; +import org.assertj.core.api.Condition; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; +import org.hamcrest.TypeSafeMatcher; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.engine.support.hierarchical.Node; + +import lombok.SneakyThrows; + +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +@DisplayName("RedBlack Tree") +class RedBlackBinarySearchTreeTest { + + @Nested + @Order(1) + @DisplayName("1. Nested classes tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class NodeClassTest { + + @Test + @Order(1) + @DisplayName("Color is a enum") + @SneakyThrows + void colorIsEnum() { + var colorClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, COLOR_CLASS_NAME)); + + assertTrue(colorClass.isEnum()); + } + + @Test + @Order(2) + @DisplayName("Color has two values 'RED' and 'BLACK'") + @SneakyThrows + void colorHasTwoValues() { + var colorClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, COLOR_CLASS_NAME)); + var declaredFields = colorClass.getDeclaredFields(); + + assertThat(declaredFields).hasSize(3); + assertThat(declaredFields).anyMatch(f -> f.getName().equals("RED")) + .anyMatch(f -> f.getName().equals("BLACK")); + } + + @Test + @Order(3) + @DisplayName("Node is a static nested class") + @SneakyThrows + void nodeIsStaticClass() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + + assertTrue(isStatic(nodeClass.getModifiers())); + } + + @Test + @Order(4) + @DisplayName("Node class has one type parameter") + @SneakyThrows + void nodeHasOneTypeParameter() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var typeParametersList = nodeClass.getTypeParameters(); + + assertThat(typeParametersList).hasSize(1); + } + + @Test + @Order(5) + @DisplayName("Node class has 'element', 'left','right', 'parent', 'color' fields") + @SneakyThrows + void nodeHasLeftAndRightFields() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var fieldNames = Arrays.stream(nodeClass.getDeclaredFields()).map(Field::getName).toList(); + + assertThat(fieldNames).contains(NODE_ELEMENT_FIELD_NAME, NODE_RIGHT_FIELD_NAME, NODE_LEFT_FIELD_NAME, + NODE_COLOR_FIELD_NAME, NODE_PARENT_FIELD_NAME); + } + } + + + @Nested + @Order(2) + @DisplayName("2. RB-tree fields tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeFieldsTest { + + @Order(1) + @DisplayName("1. RB-tree has 'root' and 'size' fields") + @Test + public void hasRootAndSizeFields() { + var redBlackTreeClass = RedBlackBinarySearchTree.class; + var declaredFields = redBlackTreeClass.getDeclaredFields(); + + assertThat(declaredFields).anyMatch(f -> f.getName().equals(SIZE_FIELD_NAME)) + .anyMatch(f -> f.getName().equals(ROOT_FIELD_NAME)); + } + } + + @Nested + @Order(3) + @DisplayName("3. RB-tree insertion() method tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeInsertTest { + + @Order(1) + @DisplayName("1. RB-tree returns true when element is inserted") + @Test + public void returnsTrueWhenElementIsInserted() { + var tree = new RedBlackBinarySearchTree(); + var isInserted = tree.insert(1); + + assertTrue(isInserted); + } + + @Order(2) + @DisplayName("2. RB-tree throws exception when element is null") + @Test + public void throwsExceptionWhenElementIsNull() { + var tree = new RedBlackBinarySearchTree(); + + assertThatNullPointerException().isThrownBy(() -> tree.insert(null)); + } + + @Order(3) + @DisplayName("3. RB-tree returns false when duplicate element is inserted and size not growing") + @Test + public void returnsFalseWhenInsertedElementIsDuplication() { + var tree = new RedBlackBinarySearchTree(); + var firstInsert = tree.insert(10); + var secondInsert = tree.insert(10); + + assertTrue(firstInsert); + assertFalse(secondInsert); + assertThat(tree.size()).isEqualTo(1); + } + + @Order(4) + @DisplayName("4. RB-tree size is growing after insertion") + @Test + public void returnsProperSizeAfterElementsAreInserted() { + var tree = new RedBlackBinarySearchTree(); + tree.insert(42); + tree.insert(24); + tree.insert(33); + var internalSizeValue = getInternalSizeValue(tree); + + assertThat(internalSizeValue).isEqualTo(3); + } + } + + @Nested + @Order(4) + @DisplayName("4. RB-tree size() method tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeSizeTest { + + @Order(1) + @DisplayName("1. RB-tree returns size == 0 when tree is empty") + @Test + public void returnsZeroSizeWhenTreeIsEmpty() { + var tree = new RedBlackBinarySearchTree(); + var size = tree.size(); + + assertThat(size).isEqualTo(0); + } + + @Order(2) + @DisplayName("2. RB-tree returns proper size after insertion") + @Test + public void returnsProperSizeAfterInsertion() { + var tree = new RedBlackBinarySearchTree(); + tree.insert("R2-D2"); + tree.insert("C-3PO"); + tree.insert("Chewie"); + tree.insert("Obi Wan"); + + var size = tree.size(); + + assertThat(size).isEqualTo(4); + } + } + + @Nested + @Order(5) + @DisplayName("5. RB-tree contains() method tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeContainsTest { + + private static RedBlackBinarySearchTree stringTree = new RedBlackBinarySearchTree<>(); + + @BeforeAll + public static void prepareTree() { + stringTree.insert("Jon"); + stringTree.insert("Bon"); + stringTree.insert("Jovi"); + } + + + @Order(1) + @DisplayName("1. RB-tree returns true when element is present") + @Test + public void returnsTrueWhenElementIsPresent() { + assertTrue(stringTree.contains("Bon")); + } + + @Order(2) + @DisplayName("2. RB-tree returns false when element is not present") + @Test + public void returnsFalseWhenElementIsNotPresent() { + assertFalse(stringTree.contains("Betty")); + } + + @Order(3) + @DisplayName("3. RB-tree throws exception when searching element is null") + @Test + public void throwsExceptionWhenElementIsNull() { + assertThatNullPointerException().isThrownBy(() -> stringTree.contains(null)); + } + } + + @Nested + @Order(6) + @DisplayName("6. RB-tree static of() method tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeOfTest { + + @Order(1) + @DisplayName("1. RB-tree of method inserts elements and size is growing") + @Test + public void returnsTrueWhenElementIsPresent() { + var stringTree = RedBlackBinarySearchTree.of("Tinky-Winky", "Dipsy", "Laa Laa", "Po"); + + assertThat(stringTree.size()).isEqualTo(4); + } + + } + + @Nested + @Order(7) + @DisplayName("7. RB-tree depth() method tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeDepthTest { + + @Order(1) + @DisplayName("1. RB-tree returns depth = 0 when tree is empty") + @Test + public void returnsZeroDepthWhenTreeIsEmpty() { + var localTimeRedBlackTree = new RedBlackBinarySearchTree(); + + assertThat(localTimeRedBlackTree.depth()).isEqualTo(0); + } + + @Order(2) + @DisplayName("2. RB-tree depth tests") + @ParameterizedTest + @MethodSource("depthTestArguments") + public void returnsProperDepthWithReBalanceAfterInsertion(int depth, Integer... elements) { + var rbTree = RedBlackBinarySearchTree.of(elements); + + assertThat(rbTree.depth()).isEqualTo(depth); + } + + public static Stream depthTestArguments() { + return Stream.of(Arguments.of(0, new Integer[] {10}), Arguments.of(1, new Integer[] {1, 2, 3}), + Arguments.of(2, new Integer[] {10, 20, 40, 50}), + Arguments.of(3, new Integer[] {-11, 7, 15, 2, 4, 900, 77, 345, 789, 1000})); + } + + } + + @Nested + @Order(8) + @DisplayName("8. RB-tree properties tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreePropertiesTest { + + @Order(1) + @DisplayName("1. RB-tree root is always black") + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public void rootIsAlwaysBlack(RedBlackBinarySearchTree tree) { + NodeProxy root = getInternalRootField(tree); + var color = root.color(); + + assertThat(color).isEqualTo(COLOR_NAME_BLACK); + } + + @Order(2) + @DisplayName("2. RB-tree has either RED or BLACK nodes") + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public void allNodesAreEitherRedOrBlack(RedBlackBinarySearchTree tree) { + NodeProxy root = getInternalRootField(tree); + + assertNodeColors(root); + } + + private void assertNodeColors(NodeProxy iterator) { + if (iterator != null) { + assertNodeColors(iterator.left()); + + assertNotNull(iterator.color()); + Condition colorCondition = + new Condition<>(s -> s.equalsIgnoreCase(COLOR_NAME_BLACK) || s.equalsIgnoreCase(COLOR_NAME_RED), + "Color condition"); + assertThat((String) iterator.color()).is(colorCondition); + + assertNodeColors(iterator.right()); + } + } + + @Order(3) + @DisplayName("3. RB-tree RED nodes doesn't have RED children") + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public void redNodesDoesntHaveRedChildren(RedBlackBinarySearchTree tree) { + NodeProxy root = getInternalRootField(tree); + Predicate assertionPredicate = n -> n.color().equals(COLOR_NAME_BLACK) || (n.left() == null || + n.left().color().equals(COLOR_NAME_BLACK) && + (n.right() == null || n.right().color().equals(COLOR_NAME_BLACK))); + + traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root, assertionPredicate); + } + + private void traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(NodeProxy root, Predicate assertionPredicate) { + if (root != null) { + traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root.left(), assertionPredicate); + + assertThat(assertionPredicate).overridingErrorMessage( + String.format("Violation of RB-tree properties: The next RED node {%s} has at least one RED child", + root)).accepts(root); + + traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root.right(), assertionPredicate); + } + } + + @Order(4) + @DisplayName("4. RB-tree each paths from a node to any of it's descending leaves has the same number of black nodes.") + @SneakyThrows + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public void allPathsFromRootToLeavesContainsTheSameNumberOfBlackNodes(RedBlackBinarySearchTree tree) { + NodeProxy root = getInternalRootField(tree); + var nodeToDepthPairList = new ArrayList>(); + countDepthForEachLeaf(root, nodeToDepthPairList, 0); + var allLeavesDepth = nodeToDepthPairList.stream().map(Pair::getValue).toList(); + + assertThat(allLeavesDepth).containsOnly(allLeavesDepth.get(0)); + } + + private void countDepthForEachLeaf(NodeProxy node, List> nodeToDepthCounter, + int depthCounter) { + if (node != null) { + if (node.color().equals(COLOR_NAME_BLACK)) { + ++depthCounter; + } + countDepthForEachLeaf(node.left(), nodeToDepthCounter, depthCounter); + + if (node.left() == null || node.right() == null) { + nodeToDepthCounter.add(Pair.of(node, depthCounter)); + } + + countDepthForEachLeaf(node.right(), nodeToDepthCounter, depthCounter); + } + } + } + + @Nested + @Order(9) + @DisplayName("9. RB-tree inOrderTraversal() method tests") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class RedBlackBinarySearchTreeInOrderTraversalTest { + + @Order(1) + @DisplayName("1. RB-tree has correct storing order") + @SneakyThrows + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public > void hasCorrectStoredOrder(RedBlackBinarySearchTree tree) { + var container = new CopyOnWriteArrayList(); + Consumer consumer = container::add; + tree.inOrderTraversal(consumer); + MatcherAssert.assertThat(container, isInAscendingOrder()); + } + + private > Matcher> isInAscendingOrder() { + return new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(List comparables) { + for (int i = 0; i < comparables.size() - 1; i++) { + if (comparables.get(i).compareTo(comparables.get(i + 1)) >= 0) { + return false; + } + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("The order is not correct. Elements should appear in ascending order"); + } + }; + } + } + + public static final String NODE_CLASS_NAME = "Node"; + public static final String COLOR_CLASS_NAME = "Color"; + public static final String PACKAGE_NAME = "com.bobocode.cs"; + public static final String TEST_CLASS_NAME = "RedBlackBinarySearchTree"; + public static final String SIZE_FIELD_NAME = "size"; + public static final String ROOT_FIELD_NAME = "root"; + public static final String NODE_LEFT_FIELD_NAME = "left"; + public static final String NODE_RIGHT_FIELD_NAME = "right"; + public static final String NODE_PARENT_FIELD_NAME = "parent"; + public static final String NODE_ELEMENT_FIELD_NAME = "element"; + public static final String NODE_COLOR_FIELD_NAME = "color"; + public static final String COLOR_NAME_RED = "RED"; + public static final String COLOR_NAME_BLACK = "BLACK"; + + private static class ColorProxy { + private Class targetClass; + private Object target; + + private Method method; + + @SneakyThrows + public ColorProxy(Object target) { + this.targetClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, COLOR_CLASS_NAME)); + this.target = target; + this.method = targetClass.getMethod("name"); + } + + @SneakyThrows + public Object getColor() { + return method.invoke(target); + } + + + } + + private static class NodeProxy { + private Class targetClass; + private Object target; + private Field elementField; + private Field parentField; + private ColorProxy colorProxy; + private Field colorField; + private Field leftField; + private Field rightField; + + @SneakyThrows + public NodeProxy(Object target) { + Objects.requireNonNull(target); + this.targetClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + this.target = target; + this.elementField = targetClass.getDeclaredField(NODE_ELEMENT_FIELD_NAME); + this.elementField.setAccessible(true); + this.parentField = targetClass.getDeclaredField(NODE_PARENT_FIELD_NAME); + this.parentField.setAccessible(true); + this.colorField = targetClass.getDeclaredField(NODE_COLOR_FIELD_NAME); + this.colorField.setAccessible(true); + this.colorProxy = new ColorProxy(colorField.get(target)); + this.leftField = targetClass.getDeclaredField(NODE_LEFT_FIELD_NAME); + this.leftField.setAccessible(true); + this.rightField = targetClass.getDeclaredField(NODE_RIGHT_FIELD_NAME); + this.rightField.setAccessible(true); + } + + @SneakyThrows + public Object element() { + return elementField.get(target); + } + + @SneakyThrows + public Object color() { + return colorProxy.getColor(); + } + + @SneakyThrows + public NodeProxy parent() { + return Optional.ofNullable(parentField.get(target)) + .map(NodeProxy::new) + .orElse(null); + } + + @SneakyThrows + public NodeProxy left() { + return Optional.ofNullable(leftField.get(target)) + .map(NodeProxy::new) + .orElse(null); + } + + @SneakyThrows + public NodeProxy right() { + return Optional.ofNullable(rightField.get(target)) + .map(NodeProxy::new) + .orElse(null); + } + } + + @SneakyThrows + private NodeProxy getInternalRootField(RedBlackBinarySearchTree tree) { + var node = getAccessibleFieldByPredicate(tree, f -> f.getName().equals(ROOT_FIELD_NAME)).get(tree); + return new NodeProxy(node); + } + + @SneakyThrows + private int getInternalSizeValue(RedBlackBinarySearchTree tree) { + return (int) getAccessibleFieldByPredicate(tree, f -> f.getName().equals(SIZE_FIELD_NAME)).get(tree); + } + + + private Field getAccessibleFieldByPredicate(Object object, Predicate predicate) { + Field field = Arrays.stream(object.getClass().getDeclaredFields()) + .filter(predicate) + .findAny() + .orElseThrow(); + field.setAccessible(true); + return field; + } + + static class RbTreeArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(Arguments.of(RedBlackBinarySearchTree.of(1, 2, 3)), Arguments.of( + RedBlackBinarySearchTree.of("Twilight Sparkle", "Fluttershy", "Pinkie Pie", "Applejack", "Rainbow Dash", + "Princess Luna", "Sunset Shimmer", "Wonderbolts", "Firecracker Burst", "Princess Celestia")), + Arguments.of(RedBlackBinarySearchTree.of(17, 23, 36, 10, 5, 18, 44, 7, 0, 64, 3, 99)), + Arguments.of(RedBlackBinarySearchTree.of(7.62, 5.56, 12.7, 152d, 11.43, 9.0, 155d, 5.45, 75d, 80d)), + Arguments.of( + RedBlackBinarySearchTree.of(DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.TUESDAY, DayOfWeek.SATURDAY, + DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.SUNDAY)), Arguments.of(RedBlackBinarySearchTree.of( + IntStream.iterate(-10_000, i -> i + 1).limit(20_000).boxed().toArray(n -> new Integer[20_000])))); + } + } +} \ No newline at end of file diff --git a/2-0-data-structures-and-algorithms/pom.xml b/2-0-data-structures-and-algorithms/pom.xml index a08d31d22..936f7d0e9 100644 --- a/2-0-data-structures-and-algorithms/pom.xml +++ b/2-0-data-structures-and-algorithms/pom.xml @@ -11,6 +11,7 @@ 2-2-4-linked-list 2-2-5-array-list 2-2-6-binary-search-tree + 2-2-7-red-black-tree 2-2-9-hash-table data-structures-and-algorithms-util From 63dfc3f82740bb7bda9aeb97bf98dffba22b3351 Mon Sep 17 00:00:00 2001 From: Serhii Bondarenko Date: Tue, 17 Jan 2023 13:37:31 +0200 Subject: [PATCH 2/2] [GP-165] Refactored tests: - Separated Node fields tests - RB-tree properties tests moved to insertion method tests - Added "algorithm step-by-step" tests - minor refactorings --- .../bobocode/cs/RedBlackBinarySearchTree.java | 41 +- .../cs/RedBlackBinarySearchTreeTest.java | 758 ++++++++++++------ .../bobocode/reflection/ReflectionUtils.java | 18 + 3 files changed, 575 insertions(+), 242 deletions(-) create mode 100644 java-fundamentals-util/src/main/java/com/bobocode/reflection/ReflectionUtils.java diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java index 5f1425387..23f5374cb 100644 --- a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/main/java/com/bobocode/cs/RedBlackBinarySearchTree.java @@ -7,29 +7,32 @@ /** * {@link RedBlackBinarySearchTree} is an implementation of a {@link BinarySearchTree} that is based on nodes, * coloring, rotations and recursion. A tree node is represented as a nested class {@link Node}. It holds an element (a value), - * references to the parent node, left/right child nodes and also a field type of {@link Color} which is Enum with two possible values RED and BLACK. - * The BinarySearch tree considered as a RedBlackTree when it meets next properties: - * 1. Every {@link Node} is either RED or BLACK {@link Color}. - * 2. RED node can't have RED children nodes - * 3. The root is always BLACK. - * 4. For each node, all simple paths from the node to descendant leaves(null nodes) contain the same number of BLACK nodes - * 5. Every leaf(null node) is null and should be treated as a BLACK node + * references to the parent node, left/right child nodes and also a field type of {@link Color} which is Enum + * with two possible values RED and BLACK
+ * The BinarySearch tree considered as a RedBlackTree when it meets next properties:
+ * 1. Every {@link Node} is either RED or BLACK {@link Color}
+ * 2. RED node can't have RED children nodes
+ * 3. The root is always BLACK
+ * 4. For each node, all simple paths from the node to descendant leaves(null nodes) contain the same number of BLACK nodes
+ * 5. Every leaf(null node) is null and should be treated as a BLACK node
* * To fulfill these requirements the tree need additional steps after insertion of new node to re-balance itself using * recoloring of nodes and rotations. Every newly inserted node should be RED before re-balancing. * - * Red-Black Tree example: - * 7(B) - * / \ - * / \ - * 3(B) 10(R) - * / \ / \ - * / \ / \ - * 2(R) null 9(B) 12(B) - * / \ / \ / \ - * / \ / \ / \ - * null null null null null 33(R) - * + * Red-Black Tree example:
+ *.........................7(B).................................
+ *..................../..........\..............................
+ *.................../............\.............................
+ *.................3(B)............10(R)........................
+ *................/...\........../......\.......................
+ *.............../.....\......../........\......................
+ *.............2(R)....null...9(B)........12(B).................
+ *............/....\........./....\....../....\.................
+ *.........../......\......./......\..../......\................
+ *.........null....null...null...null..null....33(R)............
+ *............................................/....\............
+ * ........................................../......\...........
+ * ........................................null.....null........
* *

* TODO: to get the most out of your learning, visit our website diff --git a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java index ac18f5bed..9da939c52 100644 --- a/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java +++ b/2-0-data-structures-and-algorithms/2-2-7-red-black-tree/src/test/java/com/bobocode/cs/RedBlackBinarySearchTreeTest.java @@ -41,7 +41,8 @@ import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.platform.engine.support.hierarchical.Node; + +import com.bobocode.reflection.ReflectionUtils; import lombok.SneakyThrows; @@ -57,7 +58,7 @@ class NodeClassTest { @Test @Order(1) - @DisplayName("Color is a enum") + @DisplayName("1. Color is a enum") @SneakyThrows void colorIsEnum() { var colorClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, COLOR_CLASS_NAME)); @@ -67,20 +68,20 @@ void colorIsEnum() { @Test @Order(2) - @DisplayName("Color has two values 'RED' and 'BLACK'") + @DisplayName("2. Color has two values 'RED' and 'BLACK'") @SneakyThrows void colorHasTwoValues() { var colorClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, COLOR_CLASS_NAME)); var declaredFields = colorClass.getDeclaredFields(); assertThat(declaredFields).hasSize(3); - assertThat(declaredFields).anyMatch(f -> f.getName().equals("RED")) - .anyMatch(f -> f.getName().equals("BLACK")); + assertThat(declaredFields).anyMatch(f -> f.getName().equals(COLOR_RED)) + .anyMatch(f -> f.getName().equals(COLOR_BLACK)); } @Test @Order(3) - @DisplayName("Node is a static nested class") + @DisplayName("3. Node is a static nested class") @SneakyThrows void nodeIsStaticClass() { var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); @@ -90,7 +91,7 @@ void nodeIsStaticClass() { @Test @Order(4) - @DisplayName("Node class has one type parameter") + @DisplayName("4. Node class has one type parameter") @SneakyThrows void nodeHasOneTypeParameter() { var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); @@ -101,14 +102,51 @@ void nodeHasOneTypeParameter() { @Test @Order(5) - @DisplayName("Node class has 'element', 'left','right', 'parent', 'color' fields") + @DisplayName("5. Node class has field that holds value object") + @SneakyThrows + void nodeHasElementField() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var fields = Arrays.stream(nodeClass.getDeclaredFields()).toList(); + + assertThat(fields).withFailMessage(String.format("Expected one of the %s for value field", + NODE_ELEMENT_FIELD_NAME_LIST)).anyMatch(NODE_ELEMENT_FIELD_NAME_VALIDATOR); + } + + @Test + @Order(6) + @DisplayName("6. Node class has field references to left and right nodes") @SneakyThrows void nodeHasLeftAndRightFields() { var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); - var fieldNames = Arrays.stream(nodeClass.getDeclaredFields()).map(Field::getName).toList(); + var fields = Arrays.stream(nodeClass.getDeclaredFields()).toList(); - assertThat(fieldNames).contains(NODE_ELEMENT_FIELD_NAME, NODE_RIGHT_FIELD_NAME, NODE_LEFT_FIELD_NAME, - NODE_COLOR_FIELD_NAME, NODE_PARENT_FIELD_NAME); + assertThat(fields).withFailMessage(String.format("Expected the next field names for left and right nodes %s, %s", + NODE_LEFT_FIELD_NAME_LIST, NODE_RIGHT_FIELD_NAME_LIST)) + .anyMatch(NODE_LEFT_FIELD_NAME_VALIDATOR).anyMatch(NODE_RIGHT_FIELD_NAME_VALIDATOR); + } + + @Test + @Order(7) + @DisplayName("7. Node class has 'parent' field") + @SneakyThrows + void nodeHasParentField() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var fields = Arrays.stream(nodeClass.getDeclaredFields()).toList(); + + assertThat(fields).withFailMessage("Expected %s for parent field name", NODE_PARENT_FIELD_NAME_LIST) + .anyMatch(NODE_PARENT_FIELD_NAME_VALIDATOR); + } + + @Test + @Order(8) + @DisplayName("8. Node class has 'color' field") + @SneakyThrows + void nodeHasColorField() { + var nodeClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); + var fields = Arrays.stream(nodeClass.getDeclaredFields()).toList(); + + assertThat(fields).withFailMessage("Expected %s for color field name", NODE_COLOR_FIELD_NAME_LIST) + .anyMatch(NODE_COLOR_FIELD_NAME_VALIDATOR); } } @@ -120,20 +158,29 @@ void nodeHasLeftAndRightFields() { class RedBlackBinarySearchTreeFieldsTest { @Order(1) - @DisplayName("1. RB-tree has 'root' and 'size' fields") + @DisplayName("1. RB-tree has 'root' field") + @Test + public void hasRootField() { + var redBlackTreeClass = RedBlackBinarySearchTree.class; + var declaredFields = redBlackTreeClass.getDeclaredFields(); + + assertThat(declaredFields).anyMatch(f -> f.getName().equals(SIZE_FIELD_NAME)); + } + + @Order(2) + @DisplayName("2. RB-tree has 'size' field") @Test - public void hasRootAndSizeFields() { + public void hasSizeField() { var redBlackTreeClass = RedBlackBinarySearchTree.class; var declaredFields = redBlackTreeClass.getDeclaredFields(); - assertThat(declaredFields).anyMatch(f -> f.getName().equals(SIZE_FIELD_NAME)) - .anyMatch(f -> f.getName().equals(ROOT_FIELD_NAME)); + assertThat(declaredFields).anyMatch(f -> f.getName().equals(SIZE_FIELD_NAME)); } } @Nested @Order(3) - @DisplayName("3. RB-tree insertion() method tests") + @DisplayName("3. RB-tree insertion method and re-balancing tests") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class RedBlackBinarySearchTreeInsertTest { @@ -157,7 +204,7 @@ public void throwsExceptionWhenElementIsNull() { } @Order(3) - @DisplayName("3. RB-tree returns false when duplicate element is inserted and size not growing") + @DisplayName("3. RB-tree returns false when duplicate element is inserted") @Test public void returnsFalseWhenInsertedElementIsDuplication() { var tree = new RedBlackBinarySearchTree(); @@ -166,197 +213,346 @@ public void returnsFalseWhenInsertedElementIsDuplication() { assertTrue(firstInsert); assertFalse(secondInsert); - assertThat(tree.size()).isEqualTo(1); } @Order(4) - @DisplayName("4. RB-tree size is growing after insertion") + @DisplayName("4. RB-tree containing elements after insertion") @Test - public void returnsProperSizeAfterElementsAreInserted() { + public void elementsContainingInTreeAfterInsertion() { var tree = new RedBlackBinarySearchTree(); - tree.insert(42); - tree.insert(24); - tree.insert(33); - var internalSizeValue = getInternalSizeValue(tree); + int first = 42, second = 24, third = 33; + tree.insert(first); + tree.insert(second); + tree.insert(third); - assertThat(internalSizeValue).isEqualTo(3); + assertNotNull(getElementFromTree(tree, first)); + assertNotNull(getElementFromTree(tree, second)); + assertNotNull(getElementFromTree(tree, third)); } - } - - @Nested - @Order(4) - @DisplayName("4. RB-tree size() method tests") - @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class RedBlackBinarySearchTreeSizeTest { - @Order(1) - @DisplayName("1. RB-tree returns size == 0 when tree is empty") + @Order(5) + @DisplayName("5. RB-tree size growing after insertion") @Test - public void returnsZeroSizeWhenTreeIsEmpty() { + public void sizeIsGrowingAfterInsertion() { var tree = new RedBlackBinarySearchTree(); - var size = tree.size(); + int first = 1, second = 2; + tree.insert(first); + tree.insert(second); - assertThat(size).isEqualTo(0); - } - - @Order(2) - @DisplayName("2. RB-tree returns proper size after insertion") - @Test - public void returnsProperSizeAfterInsertion() { - var tree = new RedBlackBinarySearchTree(); - tree.insert("R2-D2"); - tree.insert("C-3PO"); - tree.insert("Chewie"); - tree.insert("Obi Wan"); - - var size = tree.size(); - - assertThat(size).isEqualTo(4); - } - } - - @Nested - @Order(5) - @DisplayName("5. RB-tree contains() method tests") - @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class RedBlackBinarySearchTreeContainsTest { - - private static RedBlackBinarySearchTree stringTree = new RedBlackBinarySearchTree<>(); - - @BeforeAll - public static void prepareTree() { - stringTree.insert("Jon"); - stringTree.insert("Bon"); - stringTree.insert("Jovi"); + var internalSizeValue = getInternalSizeValue(tree); + assertThat(internalSizeValue).isEqualTo(2); } - @Order(1) - @DisplayName("1. RB-tree returns true when element is present") + @Order(6) + @DisplayName("6. After insertion of a new element the recoloring of the nodes occurs during tree re-balancing") @Test - public void returnsTrueWhenElementIsPresent() { - assertTrue(stringTree.contains("Bon")); - } - - @Order(2) - @DisplayName("2. RB-tree returns false when element is not present") + public void recoloringOccurs() { + //Insert 10 -> 5 -> 15 + int first = 10, second = 5, third = 15, fourth = 20; + var integerTree = new RedBlackBinarySearchTree(); + + integerTree.insert(first); + integerTree.insert(second); + integerTree.insert(third); + + //The root is black + var rootNode = getNodeByElement(integerTree, first); + assertThat(rootNode.color()).isEqualTo(COLOR_BLACK); + + //Children of the root is RED + var leftChild = rootNode.left(); + var rightChild = rootNode.right(); + assertThat(leftChild.color()).isEqualTo(COLOR_RED); + assertThat(rightChild.color()).isEqualTo(COLOR_RED); + + //Inserting new element(20) should invoke recolouring + //......10(B).......................10(B)......... + //...../.....\...................../....\........ + //....5(R)...15(R)........---->...5(B)...15(B)... + ///............\............................\.... + //..............20(R).......................20(R) + integerTree.insert(fourth); + var insertedNode = getNodeByElement(integerTree, fourth); + + assertThat(rootNode.color()).isEqualTo(COLOR_BLACK); + assertThat(leftChild.color()).isEqualTo(COLOR_BLACK); + assertThat(rightChild.color()).isEqualTo(COLOR_BLACK); + assertThat(insertedNode.color()).isEqualTo(COLOR_RED); + assertThat(rightChild.right().target()).isEqualTo(insertedNode.target()); + } + + @Order(7) + @DisplayName("7. After insertion of a new element the tree is balanced through right rotation") @Test - public void returnsFalseWhenElementIsNotPresent() { - assertFalse(stringTree.contains("Betty")); - } - - @Order(3) - @DisplayName("3. RB-tree throws exception when searching element is null") + public void elementInsertedAsLeftChildOfParentAndParentIsLeftChildOfGrandParent() { + int first = 10, second = 5, third = 0; + + var integerTree = new RedBlackBinarySearchTree(); + integerTree.insert(first); + integerTree.insert(second); + + var startingRoot = getNodeByElement(integerTree, first); + var startingRootLeftChild = getNodeByElement(integerTree, second); + assertThat(startingRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRootLeftChild.color()).isEqualTo(COLOR_RED); + assertThat(startingRoot.left().target()).isEqualTo(startingRootLeftChild.target()); + + //Inserting new node should invoke right-rotation over a current root node + //Insertion: 10 -> 5 -> 0 + //Re-balancing through right rotation: Rotate right over the node(10). Recolor node(10) and node(5) + //......10(B)............5(B)...... + //...../................/....\..... + //....5(R).......-->...0(R)...10(R) + ///../............................. + //.0(R)............................ + integerTree.insert(third); + var insertedNode = getNodeByElement(integerTree, third); + + //Node(5) should become new root, the newly inserted node will be left child of new root + // and old root becomes right child of new root + var newRoot = getInternalRootField(integerTree); + assertThat(newRoot.target()).isEqualTo(startingRootLeftChild.target()); + assertThat(newRoot.left().target()).isEqualTo(insertedNode.target()); + assertThat(newRoot.right().target()).isEqualTo(startingRoot.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRoot.color()).isEqualTo(COLOR_RED); + assertThat(insertedNode.color()).isEqualTo(COLOR_RED); + } + + + @Order(8) + @DisplayName("8. After insertion of a new element the tree is balanced through left-right rotation") @Test - public void throwsExceptionWhenElementIsNull() { - assertThatNullPointerException().isThrownBy(() -> stringTree.contains(null)); - } - } - - @Nested - @Order(6) - @DisplayName("6. RB-tree static of() method tests") - @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class RedBlackBinarySearchTreeOfTest { - - @Order(1) - @DisplayName("1. RB-tree of method inserts elements and size is growing") + public void elementInsertedAsRightChildOfParentAndParentIsLeftChildOfGrandParent() { + //Insertion: 10 -> 5 -> 7 + int first = 10, second = 5, third = 7; + var integerTree = new RedBlackBinarySearchTree(); + + integerTree.insert(first); + integerTree.insert(second); + + var startingRoot = getNodeByElement(integerTree, first); + var startingRootLeftChild = getNodeByElement(integerTree, second); + + //Inserting new node should invoke left-right rotation + //Re-balancing through left-right rotation: Rotate left over node(5) and then right rotate over node(10) + //Recolor node(7) and node(10) + //.....10(B)............7(B)...... + //..../................/....\..... + //...5(R).......-->...5(R)...10(R) + //....\........................... + //.....7(R)....................... + integerTree.insert(third); + var newNode = getNodeByElement(integerTree, third); + + // Newly inserted node(7) should become new root, the node(10) will be right child of new root + // and node(5) will be left child of new root + var newRoot = getInternalRootField(integerTree); + assertThat(newRoot.target()).isEqualTo(newNode.target()); + assertThat(newRoot.left().target()).isEqualTo(startingRootLeftChild.target()); + assertThat(newRoot.right().target()).isEqualTo(startingRoot.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRootLeftChild.color()).isEqualTo(COLOR_RED); + assertThat(startingRoot.color()).isEqualTo(COLOR_RED); + } + + + @Order(9) + @DisplayName("9. After insertion of a new element the tree is balanced through left rotation") @Test - public void returnsTrueWhenElementIsPresent() { - var stringTree = RedBlackBinarySearchTree.of("Tinky-Winky", "Dipsy", "Laa Laa", "Po"); - - assertThat(stringTree.size()).isEqualTo(4); - } - - } - - @Nested - @Order(7) - @DisplayName("7. RB-tree depth() method tests") - @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class RedBlackBinarySearchTreeDepthTest { - - @Order(1) - @DisplayName("1. RB-tree returns depth = 0 when tree is empty") + public void elementInsertedAsARightChildOfParentAndParentIsRightChildOfGrandParent() { + //Insertion: 0 -> 10 -> 20 + int first = 0, second = 10, third = 20; + var integerTree = new RedBlackBinarySearchTree(); + + integerTree.insert(first); + integerTree.insert(second); + + var startingRoot = getNodeByElement(integerTree, first); + var startingRootRightChild = getNodeByElement(integerTree, second); + assertThat(startingRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRootRightChild.color()).isEqualTo(COLOR_RED); + assertThat(startingRoot.right().target()).isEqualTo(startingRootRightChild.target()); + + //Inserting new node should invoke left-rotation + //Re-balancing through left rotation: Rotate left over the node(0). Recolor node(0) and node(10) + //.......0(B)...............10(B)..... + //..........\............../....\..... + //.........10(R).....-->..0(R)...20(R) + ///...........\....................... + //............20(R)................... + integerTree.insert(third); + var insertedNode = getNodeByElement(integerTree, third); + + //Node(10) should become new root, node(20) will be right child of new root + // and node(0) becomes left child of new root + var newRoot = getInternalRootField(integerTree); + assertThat(newRoot.target()).isEqualTo(startingRootRightChild.target()); + assertThat(newRoot.right().target()).isEqualTo(insertedNode.target()); + assertThat(newRoot.left().target()).isEqualTo(startingRoot.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRoot.color()).isEqualTo(COLOR_RED); + assertThat(insertedNode.color()).isEqualTo(COLOR_RED); + } + + + @Order(10) + @DisplayName("10. After insertion of a new element the tree is balanced through right-left rotation") @Test - public void returnsZeroDepthWhenTreeIsEmpty() { - var localTimeRedBlackTree = new RedBlackBinarySearchTree(); - - assertThat(localTimeRedBlackTree.depth()).isEqualTo(0); - } + public void elementInsertedAsLeftChildOfParentAndParentIsRightChildOfGrandParent() { + //Insertion: 0 -> 10 -> 5 + int first = 0, second = 10, third = 5; + var integerTree = new RedBlackBinarySearchTree(); + + integerTree.insert(first); + integerTree.insert(second); + + var startingRoot = getNodeByElement(integerTree, first); + var startingRootRightChild = getNodeByElement(integerTree, second); + + //Inserting new node should invoke right-left rotation + //Re-balancing through right-left rotation: Rotate right over the node(10) ant then rotate left over node(0). + // Recolor node(0) and node(5) + //.......0(B)...............5(B)...... + //..........\............../....\..... + //.........10(R).....-->..0(R)...10(R) + ///......./........................... + //......5(R).......................... + integerTree.insert(third); + var newNode = getNodeByElement(integerTree, third); + + // Newly inserted node(5) should become new root, the node(10) will be right child of new root + // and node(0) will be left child of new root + var newRoot = getInternalRootField(integerTree); + assertThat(newRoot.target()).isEqualTo(newNode.target()); + assertThat(newRoot.right().target()).isEqualTo(startingRootRightChild.target()); + assertThat(newRoot.left().target()).isEqualTo(startingRoot.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(startingRootRightChild.color()).isEqualTo(COLOR_RED); + assertThat(startingRoot.color()).isEqualTo(COLOR_RED); + } + + + @Order(11) + @DisplayName("11. Recoloring is invoking re-balance on the top recoloured node") + @Test + public void rotationOccursAfterRecolouring() { + //Insertion -> 5, 10, 17, 15, 25, 20, 28 + var startingValues = List.of(5, 10, 17, 15, 25, 20, 28); + int newValue = 30; + var integerTree = new RedBlackBinarySearchTree(); + for (var value: + startingValues) { + integerTree.insert(value); + } - @Order(2) - @DisplayName("2. RB-tree depth tests") + //Insertion of new node(30) should invoke re-coloring and then rotation: + //...............10(B)..................................................17(B)......................... + //............../.....\.............................................../........\...................... + //............./.......\............................................./......... \..................... + //...........5(B).....17(R)........................................10(R)....... 25(R)................. + //.................../.....\...............------->............../......\......./....\................ + //................../.......\.................................../........\...../......\............... + //................15(B).....25(B)..............................5(B)....15(B)..20(B)...28(B)........... + //........................./.....\......................................................\............. + //......................../.......\......................................................\............ + //.......................20(R)...28(R)...................................................30(R)........ + //..................................\................................................................. + //...................................\................................................................ + //...................................30(R)............................................................ + integerTree.insert(newValue); + var node30 = getNodeByElement(integerTree, newValue); + var newRoot = getInternalRootField(integerTree); + var node17 = getNodeByElement(integerTree, 17); + var node10 = getNodeByElement(integerTree, 10); + var node25 = getNodeByElement(integerTree, 25); + var node15 = getNodeByElement(integerTree, 15); + var node28 = getNodeByElement(integerTree, 28); + + assertThat(newRoot.target()).isEqualTo(node17.target()); + assertThat(newRoot.left().target()).isEqualTo(node10.target()); + assertThat(newRoot.right().target()).isEqualTo(node25.target()); + assertThat(node10.right().target()).isEqualTo(node15.target()); + assertThat(node30.parent().target()).isEqualTo(node28.target()); + assertThat(newRoot.color()).isEqualTo(COLOR_BLACK); + assertThat(node10.color()).isEqualTo(COLOR_RED); + assertThat(node25.color()).isEqualTo(COLOR_RED); + assertThat(node15.color()).isEqualTo(COLOR_BLACK); + assertThat(node28.color()).isEqualTo(COLOR_BLACK); + } + + + @Order(12) + @DisplayName("12. RB-tree root is always black for different inputs") @ParameterizedTest - @MethodSource("depthTestArguments") - public void returnsProperDepthWithReBalanceAfterInsertion(int depth, Integer... elements) { - var rbTree = RedBlackBinarySearchTree.of(elements); + @ArgumentsSource(RbTreeArgumentProvider.class) + public > void rootIsAlwaysBlack(List elementsList) { + var tree = fillTree(elementsList); + NodeProxy root = getInternalRootField(tree); + var color = root.color(); - assertThat(rbTree.depth()).isEqualTo(depth); + assertThat(color).isEqualTo(COLOR_BLACK); } - public static Stream depthTestArguments() { - return Stream.of(Arguments.of(0, new Integer[] {10}), Arguments.of(1, new Integer[] {1, 2, 3}), - Arguments.of(2, new Integer[] {10, 20, 40, 50}), - Arguments.of(3, new Integer[] {-11, 7, 15, 2, 4, 900, 77, 345, 789, 1000})); - } + @Order(13) + @DisplayName("13. RB-tree has only RED or BLACK nodes for different inputs") + @ParameterizedTest + @ArgumentsSource(RbTreeArgumentProvider.class) + public > void allNodesAreEitherRedOrBlack(List elementsList) { + var tree = fillTree(elementsList); + NodeProxy root = getInternalRootField(tree); - } + Condition colorCondition = + new Condition<>(s -> s.equalsIgnoreCase(COLOR_BLACK) || s.equalsIgnoreCase(COLOR_RED), + "Color condition"); - @Nested - @Order(8) - @DisplayName("8. RB-tree properties tests") - @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class RedBlackBinarySearchTreePropertiesTest { + assertNodeColors(root, colorCondition); + } - @Order(1) - @DisplayName("1. RB-tree root is always black") + @Order(14) + @DisplayName("14. RB-tree RED nodes doesn't have RED children for different inputs") @ParameterizedTest @ArgumentsSource(RbTreeArgumentProvider.class) - public void rootIsAlwaysBlack(RedBlackBinarySearchTree tree) { + public > void redNodesDoesntHaveRedChildren(List elementsList) { + var tree = fillTree(elementsList); NodeProxy root = getInternalRootField(tree); - var color = root.color(); - assertThat(color).isEqualTo(COLOR_NAME_BLACK); + Predicate assertionPredicate = n -> n.color().equals(COLOR_BLACK) || (n.left() == null || + n.left().color().equals(COLOR_BLACK) && + (n.right() == null || n.right().color().equals(COLOR_BLACK))); + + traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root, assertionPredicate); } - @Order(2) - @DisplayName("2. RB-tree has either RED or BLACK nodes") + @Order(15) + @DisplayName("15. RB-tree each paths from a node to any of it's descending leaves has the same number " + + "of black nodes for different inputs") + @SneakyThrows @ParameterizedTest @ArgumentsSource(RbTreeArgumentProvider.class) - public void allNodesAreEitherRedOrBlack(RedBlackBinarySearchTree tree) { + public > void allPathsFromRootToLeavesContainsTheSameNumberOfBlackNodes(List elementsList) { + var tree = fillTree(elementsList); NodeProxy root = getInternalRootField(tree); - assertNodeColors(root); + var nodeToDepthPairList = new ArrayList>(); + countDepthForEachLeaf(root, nodeToDepthPairList, 0); + var allLeavesDepth = nodeToDepthPairList.stream().map(Pair::getValue).toList(); + + assertThat(allLeavesDepth).containsOnly(allLeavesDepth.get(0)); } - private void assertNodeColors(NodeProxy iterator) { + private void assertNodeColors(NodeProxy iterator, Condition colorCondition) { if (iterator != null) { - assertNodeColors(iterator.left()); + assertNodeColors(iterator.left(), colorCondition); assertNotNull(iterator.color()); - Condition colorCondition = - new Condition<>(s -> s.equalsIgnoreCase(COLOR_NAME_BLACK) || s.equalsIgnoreCase(COLOR_NAME_RED), - "Color condition"); assertThat((String) iterator.color()).is(colorCondition); - assertNodeColors(iterator.right()); + assertNodeColors(iterator.right(), colorCondition); } } - @Order(3) - @DisplayName("3. RB-tree RED nodes doesn't have RED children") - @ParameterizedTest - @ArgumentsSource(RbTreeArgumentProvider.class) - public void redNodesDoesntHaveRedChildren(RedBlackBinarySearchTree tree) { - NodeProxy root = getInternalRootField(tree); - Predicate assertionPredicate = n -> n.color().equals(COLOR_NAME_BLACK) || (n.left() == null || - n.left().color().equals(COLOR_NAME_BLACK) && - (n.right() == null || n.right().color().equals(COLOR_NAME_BLACK))); - - traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root, assertionPredicate); - } - private void traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(NodeProxy root, Predicate assertionPredicate) { if (root != null) { traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(root.left(), assertionPredicate); @@ -369,24 +565,10 @@ private void traverseTreeAndAssertThatRedNodesDoesntHaveRedChildren(NodeProxy ro } } - @Order(4) - @DisplayName("4. RB-tree each paths from a node to any of it's descending leaves has the same number of black nodes.") - @SneakyThrows - @ParameterizedTest - @ArgumentsSource(RbTreeArgumentProvider.class) - public void allPathsFromRootToLeavesContainsTheSameNumberOfBlackNodes(RedBlackBinarySearchTree tree) { - NodeProxy root = getInternalRootField(tree); - var nodeToDepthPairList = new ArrayList>(); - countDepthForEachLeaf(root, nodeToDepthPairList, 0); - var allLeavesDepth = nodeToDepthPairList.stream().map(Pair::getValue).toList(); - - assertThat(allLeavesDepth).containsOnly(allLeavesDepth.get(0)); - } - private void countDepthForEachLeaf(NodeProxy node, List> nodeToDepthCounter, int depthCounter) { if (node != null) { - if (node.color().equals(COLOR_NAME_BLACK)) { + if (node.color().equals(COLOR_BLACK)) { ++depthCounter; } countDepthForEachLeaf(node.left(), nodeToDepthCounter, depthCounter); @@ -398,20 +580,107 @@ private void countDepthForEachLeaf(NodeProxy node, List countDepthForEachLeaf(node.right(), nodeToDepthCounter, depthCounter); } } + } @Nested - @Order(9) - @DisplayName("9. RB-tree inOrderTraversal() method tests") + @Order(4) + @DisplayName("4. RB-tree other methods tests") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class RedBlackBinarySearchTreeInOrderTraversalTest { + class RedBlackBinarySearchTreeSizeTest { + + private static RedBlackBinarySearchTree stringTree = new RedBlackBinarySearchTree<>(); + + @BeforeAll + public static void prepareTree() { + stringTree.insert("Jon"); + stringTree.insert("Bon"); + stringTree.insert("Jovi"); + } @Order(1) - @DisplayName("1. RB-tree has correct storing order") + @DisplayName("1. size() method. RB-tree returns size == 0 when tree is empty") + @Test + public void returnsZeroSizeWhenTreeIsEmpty() { + var tree = new RedBlackBinarySearchTree(); + var size = tree.size(); + + assertThat(size).isEqualTo(0); + } + + @Order(2) + @DisplayName("2. size() method. RB-tree returns proper size after insertion") + @Test + public void returnsProperSizeAfterInsertion() { + var tree = new RedBlackBinarySearchTree(); + tree.insert("R2-D2"); + tree.insert("C-3PO"); + tree.insert("Chewie"); + tree.insert("Obi Wan"); + + var size = tree.size(); + + assertThat(size).isEqualTo(4); + } + + + @Order(3) + @DisplayName("3. contains() method. RB-tree returns true when element is present") + @Test + public void returnsTrueWhenElementIsPresent() { + assertTrue(stringTree.contains("Bon")); + } + + @Order(4) + @DisplayName("4. contains() method. RB-tree returns false when element is not present") + @Test + public void returnsFalseWhenElementIsNotPresent() { + assertFalse(stringTree.contains("Betty")); + } + + @Order(5) + @DisplayName("5. contains() method. RB-tree throws exception when searching element is null") + @Test + public void throwsExceptionWhenElementIsNull() { + assertThatNullPointerException().isThrownBy(() -> stringTree.contains(null)); + } + + @Order(6) + @DisplayName("6. of() method. RB-tree of() method creates new tree with proper size") + @Test + public void ofMethodCreatesTreeWithProperSize() { + var stringTree = RedBlackBinarySearchTree.of("Tinky-Winky", "Dipsy", "Laa Laa", "Po"); + + assertThat(stringTree.size()).isEqualTo(4); + + } + + @Order(7) + @DisplayName("7. depth() method. RB-tree returns depth == 0 when tree is empty") + @Test + public void returnsZeroDepthWhenTreeIsEmpty() { + var localTimeRedBlackTree = new RedBlackBinarySearchTree(); + + assertThat(localTimeRedBlackTree.depth()).isEqualTo(0); + } + + @Order(8) + @DisplayName("8. depth() method. RB-tree depth tests") + @ParameterizedTest + @MethodSource("depthTestArguments") + public void returnsProperDepthWithReBalanceAfterInsertion(int depth, Integer... elements) { + var rbTree = RedBlackBinarySearchTree.of(elements); + + assertThat(rbTree.depth()).isEqualTo(depth); + } + + @Order(9) + @DisplayName("9. inOrderTraversal() method. RB-tree has correct storing order") @SneakyThrows @ParameterizedTest @ArgumentsSource(RbTreeArgumentProvider.class) - public > void hasCorrectStoredOrder(RedBlackBinarySearchTree tree) { + public > void hasCorrectStoredOrder(List elementsList) { + var tree = fillTree(elementsList); var container = new CopyOnWriteArrayList(); Consumer consumer = container::add; tree.inOrderTraversal(consumer); @@ -436,6 +705,14 @@ public void describeTo(Description description) { } }; } + + + public static Stream depthTestArguments() { + return Stream.of(Arguments.of(0, new Integer[] {10}), Arguments.of(1, new Integer[] {1, 2, 3}), + Arguments.of(2, new Integer[] {10, 20, 40, 50}), + Arguments.of(3, new Integer[] {-11, 7, 15, 2, 4, 900, 77, 345, 789, 1000})); + } + } public static final String NODE_CLASS_NAME = "Node"; @@ -444,18 +721,24 @@ public void describeTo(Description description) { public static final String TEST_CLASS_NAME = "RedBlackBinarySearchTree"; public static final String SIZE_FIELD_NAME = "size"; public static final String ROOT_FIELD_NAME = "root"; - public static final String NODE_LEFT_FIELD_NAME = "left"; - public static final String NODE_RIGHT_FIELD_NAME = "right"; - public static final String NODE_PARENT_FIELD_NAME = "parent"; - public static final String NODE_ELEMENT_FIELD_NAME = "element"; - public static final String NODE_COLOR_FIELD_NAME = "color"; - public static final String COLOR_NAME_RED = "RED"; - public static final String COLOR_NAME_BLACK = "BLACK"; + public static final List NODE_PARENT_FIELD_NAME_LIST = List.of("parent"); + public static final List NODE_COLOR_FIELD_NAME_LIST = List.of("color"); + public static final List NODE_LEFT_FIELD_NAME_LIST = List.of("left", "leftChild"); + public static final List NODE_RIGHT_FIELD_NAME_LIST = List.of("right", "rightChild"); + public static final List NODE_ELEMENT_FIELD_NAME_LIST = List.of("element", "data", "value", "key"); + public static final Predicate NODE_PARENT_FIELD_NAME_VALIDATOR = f -> NODE_PARENT_FIELD_NAME_LIST.contains(f.getName()); + public static final Predicate NODE_LEFT_FIELD_NAME_VALIDATOR = f -> NODE_LEFT_FIELD_NAME_LIST.contains(f.getName()); + public static final Predicate NODE_RIGHT_FIELD_NAME_VALIDATOR = f -> NODE_RIGHT_FIELD_NAME_LIST.contains(f.getName()); + public static final Predicate NODE_ELEMENT_FIELD_NAME_VALIDATOR = f -> NODE_ELEMENT_FIELD_NAME_LIST.contains(f.getName()); + public static final Predicate NODE_COLOR_FIELD_NAME_VALIDATOR = f -> NODE_COLOR_FIELD_NAME_LIST.contains(f.getName()); + public static final String COLOR_RED = "RED"; + public static final String COLOR_BLACK = "BLACK"; + private static class ColorProxy { + private Class targetClass; private Object target; - private Method method; @SneakyThrows @@ -471,36 +754,28 @@ public Object getColor() { } - } + } private static class NodeProxy { private Class targetClass; + private Object target; private Field elementField; private Field parentField; - private ColorProxy colorProxy; private Field colorField; private Field leftField; private Field rightField; - @SneakyThrows public NodeProxy(Object target) { Objects.requireNonNull(target); this.targetClass = Class.forName(String.format("%s.%s$%s", PACKAGE_NAME, TEST_CLASS_NAME, NODE_CLASS_NAME)); - this.target = target; - this.elementField = targetClass.getDeclaredField(NODE_ELEMENT_FIELD_NAME); - this.elementField.setAccessible(true); - this.parentField = targetClass.getDeclaredField(NODE_PARENT_FIELD_NAME); - this.parentField.setAccessible(true); - this.colorField = targetClass.getDeclaredField(NODE_COLOR_FIELD_NAME); - this.colorField.setAccessible(true); - this.colorProxy = new ColorProxy(colorField.get(target)); - this.leftField = targetClass.getDeclaredField(NODE_LEFT_FIELD_NAME); - this.leftField.setAccessible(true); - this.rightField = targetClass.getDeclaredField(NODE_RIGHT_FIELD_NAME); - this.rightField.setAccessible(true); + this.target = targetClass.cast(target); + this.elementField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_ELEMENT_FIELD_NAME_VALIDATOR); + this.parentField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_PARENT_FIELD_NAME_VALIDATOR); + this.leftField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_LEFT_FIELD_NAME_VALIDATOR); + this.rightField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_RIGHT_FIELD_NAME_VALIDATOR); + this.colorField = ReflectionUtils.getAccessibleFieldByPredicate(targetClass, NODE_COLOR_FIELD_NAME_VALIDATOR); } - @SneakyThrows public Object element() { return elementField.get(target); @@ -508,7 +783,7 @@ public Object element() { @SneakyThrows public Object color() { - return colorProxy.getColor(); + return new ColorProxy(colorField.get(target)).getColor(); } @SneakyThrows @@ -531,42 +806,79 @@ public NodeProxy right() { .map(NodeProxy::new) .orElse(null); } - } + public Object target() { + return target; + } + + + } @SneakyThrows private NodeProxy getInternalRootField(RedBlackBinarySearchTree tree) { - var node = getAccessibleFieldByPredicate(tree, f -> f.getName().equals(ROOT_FIELD_NAME)).get(tree); + var node = ReflectionUtils.getAccessibleFieldByPredicate(tree.getClass(), f -> f.getName().equals(ROOT_FIELD_NAME)).get(tree); return new NodeProxy(node); } @SneakyThrows private int getInternalSizeValue(RedBlackBinarySearchTree tree) { - return (int) getAccessibleFieldByPredicate(tree, f -> f.getName().equals(SIZE_FIELD_NAME)).get(tree); + return (int) ReflectionUtils.getAccessibleFieldByPredicate(tree.getClass(), f -> f.getName().equals(SIZE_FIELD_NAME)).get(tree); + } + + private > NodeProxy getNodeByElement(RedBlackBinarySearchTree tree, T element) { + var node = getInternalRootField(tree); + while (node != null) { + if (node.element().equals(element)) { + return node; + } else if (((Comparable) node.element()).compareTo(element) < 0) { + node = node.right(); + } else { + node = node.left(); + } + } + return null; + } + + private > T getElementFromTree(RedBlackBinarySearchTree tree, T element) { + var internalRootField = getInternalRootField(tree); + return findElementInTree(internalRootField, element); } + private > T findElementInTree(NodeProxy node, T element) { + while (node != null) { + if (node.element().equals(element)) { + return element; + } else if (((Comparable) node.element()).compareTo(element) < 0) { + node = node.right(); + } else { + node = node.left(); + } + } + return null; + } - private Field getAccessibleFieldByPredicate(Object object, Predicate predicate) { - Field field = Arrays.stream(object.getClass().getDeclaredFields()) - .filter(predicate) - .findAny() - .orElseThrow(); - field.setAccessible(true); - return field; + private static > RedBlackBinarySearchTree fillTree(List elementList) { + var tree = new RedBlackBinarySearchTree(); + for (var element: + elementList) { + tree.insert(element); + } + return tree; } static class RbTreeArgumentProvider implements ArgumentsProvider { + @Override public Stream provideArguments(ExtensionContext context) { - return Stream.of(Arguments.of(RedBlackBinarySearchTree.of(1, 2, 3)), Arguments.of( - RedBlackBinarySearchTree.of("Twilight Sparkle", "Fluttershy", "Pinkie Pie", "Applejack", "Rainbow Dash", + return Stream.of(Arguments.of(List.of(1, 2, 3)), Arguments.of( + List.of("Twilight Sparkle", "Fluttershy", "Pinkie Pie", "Applejack", "Rainbow Dash", "Princess Luna", "Sunset Shimmer", "Wonderbolts", "Firecracker Burst", "Princess Celestia")), - Arguments.of(RedBlackBinarySearchTree.of(17, 23, 36, 10, 5, 18, 44, 7, 0, 64, 3, 99)), - Arguments.of(RedBlackBinarySearchTree.of(7.62, 5.56, 12.7, 152d, 11.43, 9.0, 155d, 5.45, 75d, 80d)), + Arguments.of(List.of(17, 23, 36, 10, 5, 18, 44, 7, 0, 64, 3, 99)), + Arguments.of(List.of(7.62, 5.56, 12.7, 152d, 11.43, 9.0, 155d, 5.45, 75d, 80d)), Arguments.of( - RedBlackBinarySearchTree.of(DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.TUESDAY, DayOfWeek.SATURDAY, - DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.SUNDAY)), Arguments.of(RedBlackBinarySearchTree.of( - IntStream.iterate(-10_000, i -> i + 1).limit(20_000).boxed().toArray(n -> new Integer[20_000])))); + List.of(DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.TUESDAY, DayOfWeek.SATURDAY, + DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.SUNDAY)), Arguments.of( + IntStream.iterate(-10_000, i -> i + 1).limit(20_000).boxed().toList())); } } } \ No newline at end of file diff --git a/java-fundamentals-util/src/main/java/com/bobocode/reflection/ReflectionUtils.java b/java-fundamentals-util/src/main/java/com/bobocode/reflection/ReflectionUtils.java new file mode 100644 index 000000000..a7f707e85 --- /dev/null +++ b/java-fundamentals-util/src/main/java/com/bobocode/reflection/ReflectionUtils.java @@ -0,0 +1,18 @@ +package com.bobocode.reflection; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.function.Predicate; + +public class ReflectionUtils { + + public static Field getAccessibleFieldByPredicate(Class targetClass, Predicate fieldNamePredicate) { + var field = Arrays.stream(targetClass.getDeclaredFields()) + .filter(fieldNamePredicate) + .findFirst() + .orElseThrow(); + field.setAccessible(true); + return field; + } + +}