diff --git a/src/main/java/org/nintynine/problems/BtreeP67.java b/src/main/java/org/nintynine/problems/BtreeP67.java new file mode 100644 index 0000000..d141a8a --- /dev/null +++ b/src/main/java/org/nintynine/problems/BtreeP67.java @@ -0,0 +1,170 @@ +package org.nintynine.problems; + +import java.util.Objects; // For Objects.equals and Objects.hash + +public class BtreeP67 { + + static class Node { // Changed from private static to static (package-private) + T value; + Node left; + Node right; + + Node(T value) { + this.value = value; + this.left = null; + this.right = null; + } + + @Override + public String toString() { // For debugging + return "Node{" + "value=" + value + + (left != null && left.value != null ? ", L:" + left.value : "") + + (right != null && right.value != null ? ", R:" + right.value : "") + + '}'; + } + + // Node equality needed for tree equality + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Node node = (Node) o; + // Check current node's value, then recursively check children + return Objects.equals(value, node.value) && + Objects.equals(left, node.left) && // relies on Node.equals for left subtree + Objects.equals(right, node.right); // relies on Node.equals for right subtree + } + + @Override + public int hashCode() { + // Combine hash codes of value and children + return Objects.hash(value, left, right); // relies on Node.hashCode for children + } + } + + private Node root; + + public BtreeP67() { + this.root = null; + } + + // Constructor to create a tree with a given root. + public BtreeP67(Node root) { + this.root = root; + } + + // Getter for the root, might be useful for testing. + public Node getRoot() { + return root; + } + + public static BtreeP67 fromString(String representation) { + if (representation == null || representation.isEmpty()) { + return new BtreeP67<>(null); // Tree with null root for empty string + } + int[] index = {0}; // Current parsing position in the string + Node rootNode = parseInternal(representation, index); + + // After parsing, index should be at the end of the string. + // If not, it means there are unparsed characters, so the string is malformed. + if (index[0] != representation.length()) { + throw new IllegalArgumentException("Malformed tree string: unexpected characters after parsing. Index: " + index[0] + ", String length: " + representation.length() + ", String: '" + representation + "'"); + } + + return new BtreeP67<>(rootNode); + } + + private static Node parseInternal(String s, int[] index) { + // Base case: if we are at the end of string or at a separator for an empty spot + if (index[0] >= s.length()) { + // This can happen if e.g. string ends prematurely like "a(" + // The checks for comma/parenthesis later will catch this if it's mid-structure. + // If it's a valid end of a recursive call (e.g. for a null child), this is fine. + return null; + } + char currentChar = s.charAt(index[0]); + if (currentChar == ',' || currentChar == ')') { // Indicates an empty subtree spot + return null; + } + + // Parse the value of the current node (assuming single character values) + String value = String.valueOf(currentChar); + Node currentNode = new Node<>(value); + index[0]++; // Consume the character for the node's value + + // Check if this node has children, indicated by an opening parenthesis '(' + if (index[0] < s.length() && s.charAt(index[0]) == '(') { + index[0]++; // Consume '(' + + // Parse the left child. This will return null if the left child is empty (e.g., ",c"). + currentNode.left = parseInternal(s, index); + + // After parsing the left child, a comma must follow. + if (index[0] >= s.length() || s.charAt(index[0]) != ',') { + throw new IllegalArgumentException("Malformed tree string: expected ',' separator after left child/subtree for node '" + value + "' near index " + index[0] + " in '" + s + "'"); + } + index[0]++; // Consume ',' + + // Parse the right child. This will return null if the right child is empty (e.g., "b,"). + currentNode.right = parseInternal(s, index); + + // After parsing the right child, a closing parenthesis must follow. + if (index[0] >= s.length() || s.charAt(index[0]) != ')') { + throw new IllegalArgumentException("Malformed tree string: expected ')' to close children of node '" + value + "' near index " + index[0] + " in '" + s + "'"); + } + index[0]++; // Consume ')' + } + // If no '(', it's a leaf node; its left and right children remain null by default. + return currentNode; + } + + @Override + public String toString() { + if (root == null) { + return ""; // Consistent with fromString("") creating a tree with null root + } + StringBuilder sb = new StringBuilder(); + buildStringInternal(root, sb); + return sb.toString(); + } + + private void buildStringInternal(Node node, StringBuilder sb) { + // This method is called only for non-null nodes by its callers. + sb.append(node.value); + + // A node is a "parent" (needs parentheses) if it has at least one non-null child. + // If both children are null, it's a leaf node, and no parentheses are printed. + if (node.left != null || node.right != null) { + sb.append('('); + if (node.left != null) { + buildStringInternal(node.left, sb); // Recursively build string for left child + } + // If left child is null, nothing is appended for its part. + + sb.append(','); + + if (node.right != null) { + buildStringInternal(node.right, sb); // Recursively build string for right child + } + // If right child is null, nothing is appended for its part. + + sb.append(')'); + } + } + + // For comparing tree structures. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BtreeP67 that = (BtreeP67) o; + // Tree equality depends on root node equality (which is recursive) + return Objects.equals(root, that.root); + } + + @Override + public int hashCode() { + // Tree hash code depends on root node hash code (which is recursive) + return Objects.hash(root); + } +} diff --git a/src/test/java/org/nintynine/problems/BtreeP67Test.java b/src/test/java/org/nintynine/problems/BtreeP67Test.java new file mode 100644 index 0000000..a8f8af9 --- /dev/null +++ b/src/test/java/org/nintynine/problems/BtreeP67Test.java @@ -0,0 +1,223 @@ +package org.nintynine.problems; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class BtreeP67Test { + + private boolean isLeaf(BtreeP67.Node node) { + return node != null && node.left == null && node.right == null; + } + + @Test + void testFromString_complexExample() { + String s = "a(b(d,e),c(,f(g,)))"; + BtreeP67 tree = BtreeP67.fromString(s); + assertNotNull(tree.getRoot(), "Root should not be null for: " + s); + assertEquals("a", tree.getRoot().value, "Root value mismatch"); + + // Left branch: b(d,e) + BtreeP67.Node nodeB = tree.getRoot().left; + assertNotNull(nodeB, "Node B (a's left child) should not be null"); + assertEquals("b", nodeB.value, "Node B value mismatch"); + + BtreeP67.Node nodeD = nodeB.left; + assertNotNull(nodeD, "Node D (b's left child) should not be null"); + assertEquals("d", nodeD.value, "Node D value mismatch"); + assertTrue(isLeaf(nodeD), "Node D should be a leaf"); + + BtreeP67.Node nodeE = nodeB.right; + assertNotNull(nodeE, "Node E (b's right child) should not be null"); + assertEquals("e", nodeE.value, "Node E value mismatch"); + assertTrue(isLeaf(nodeE), "Node E should be a leaf"); + + // Right branch: c(,f(g,)) + BtreeP67.Node nodeC = tree.getRoot().right; + assertNotNull(nodeC, "Node C (a's right child) should not be null"); + assertEquals("c", nodeC.value, "Node C value mismatch"); + assertNull(nodeC.left, "Node C's left child should be null"); + + BtreeP67.Node nodeF = nodeC.right; + assertNotNull(nodeF, "Node F (c's right child) should not be null"); + assertEquals("f", nodeF.value, "Node F value mismatch"); + + BtreeP67.Node nodeG = nodeF.left; + assertNotNull(nodeG, "Node G (f's left child) should not be null"); + assertEquals("g", nodeG.value, "Node G value mismatch"); + assertTrue(isLeaf(nodeG), "Node G should be a leaf"); + + assertNull(nodeF.right, "Node F's right child should be null"); + } + + @Test + void testToString_complexExample() { + // Build the tree for "a(b(d,e),c(,f(g,)))" manually + BtreeP67.Node root = new BtreeP67.Node<>("a"); + root.left = new BtreeP67.Node<>("b"); + root.left.left = new BtreeP67.Node<>("d"); + root.left.right = new BtreeP67.Node<>("e"); + root.right = new BtreeP67.Node<>("c"); + root.right.right = new BtreeP67.Node<>("f"); + root.right.right.left = new BtreeP67.Node<>("g"); + BtreeP67 tree = new BtreeP67<>(root); + assertEquals("a(b(d,e),c(,f(g,)))", tree.toString()); + } + + @Test + void testFromString_singleNode() { + BtreeP67 tree = BtreeP67.fromString("a"); + assertNotNull(tree.getRoot(), "Root of single node tree 'a' should not be null"); + assertEquals("a", tree.getRoot().value); + assertTrue(isLeaf(tree.getRoot()), "Single node 'a' should be a leaf"); + } + + @Test + void testFromString_leftHeavy() { + // a has left child b, b has left child c. c is a leaf. b has no right child. a has no right child. + String s = "a(b(c,),)"; + BtreeP67 tree = BtreeP67.fromString(s); + assertNotNull(tree.getRoot(), "Root for 'a(b(c,),)' should not be null"); + assertEquals("a", tree.getRoot().value); + + BtreeP67.Node nodeB = tree.getRoot().left; + assertNotNull(nodeB, "Node B for 'a(b(c,),)' should not be null"); + assertEquals("b", nodeB.value); + assertNull(tree.getRoot().right, "Root 'a' should have no right child in 'a(b(c,),)'"); + + BtreeP67.Node nodeC = nodeB.left; + assertNotNull(nodeC, "Node C for 'a(b(c,),)' should not be null"); + assertEquals("c", nodeC.value); + assertNull(nodeB.right, "Node B should have no right child in 'a(b(c,),)'"); + assertTrue(isLeaf(nodeC), "Node C should be a leaf in 'a(b(c,),)'"); + } + + @Test + void testRoundTripConversions() { + String[] testStrings = { + "a(b(d,e),c(,f(g,)))", + "a", + "x(y,z)", + "a(b,)", + "a(,c)", + "m(n(o,p),q(r(s,t),u))", + "a(b(c(d(e,),),),)" + }; + + for (String s : testStrings) { + BtreeP67 tree = BtreeP67.fromString(s); + String roundTripString = tree.toString(); + assertEquals(s, roundTripString, "Round trip toString(fromString(s)) failed for: " + s); + + BtreeP67 treeFromRoundTripString = BtreeP67.fromString(roundTripString); + assertEquals(tree, treeFromRoundTripString, "Round trip fromString(toString(tree)) failed for: " + s); + } + + BtreeP67 treeFromVerboseLeaf = BtreeP67.fromString("z(,)"); + assertEquals("z", treeFromVerboseLeaf.toString(), "Tree from 'z(,)' should stringify to 'z'"); + BtreeP67 treeFromSimpleLeaf = BtreeP67.fromString("z"); + assertEquals(treeFromSimpleLeaf, treeFromVerboseLeaf, "Tree from 'z(,)' should be equal to tree from 'z'"); + } + + @Test + void testToStringForManuallyConstructedTrees() { + BtreeP67 leaf = new BtreeP67<>(new BtreeP67.Node<>("x")); + assertEquals("x", leaf.toString()); + + BtreeP67.Node rootLeft = new BtreeP67.Node<>("p"); + rootLeft.left = new BtreeP67.Node<>("q"); + BtreeP67 treeLeft = new BtreeP67<>(rootLeft); + assertEquals("p(q,)", treeLeft.toString()); + + BtreeP67.Node rootRight = new BtreeP67.Node<>("m"); + rootRight.right = new BtreeP67.Node<>("n"); + BtreeP67 treeRight = new BtreeP67<>(rootRight); + assertEquals("m(,n)", treeRight.toString()); + + BtreeP67.Node rootTwo = new BtreeP67.Node<>("k"); + rootTwo.left = new BtreeP67.Node<>("l"); + rootTwo.right = new BtreeP67.Node<>("m"); + BtreeP67 treeTwo = new BtreeP67<>(rootTwo); + assertEquals("k(l,m)", treeTwo.toString()); + + BtreeP67.Node rootWithEmptyChildren = new BtreeP67.Node<>("j"); + BtreeP67 treeEmptyChildren = new BtreeP67<>(rootWithEmptyChildren); + assertEquals("j", treeEmptyChildren.toString()); + } + + @Test + void testFromString_emptyString() { + BtreeP67 tree = BtreeP67.fromString(""); + assertNull(tree.getRoot(), "Tree from empty string should have null root"); + assertEquals("", tree.toString(), "toString of empty tree should be empty string"); + } + + @Test + void testFromString_malformedStrings() { + String[] malformed = { + "a(b", "a(b,c", "a(b,,c)", "a(b,c)d", "()", "a(()b)", "a((b),c)", + "a(b,c))", "a(b,c,", "a(,", "a(b c)", "a(b,(c,d)", "a(b(", "a(b,c(", + "a(b,c) ", " a(b,c)" + }; + + for (String s : malformed) { + final String currentInput = s; + assertThrows(IllegalArgumentException.class, () -> BtreeP67.fromString(currentInput), + "Expected IllegalArgumentException for malformed string: \"" + currentInput + "\""); + } + } + + @Test + void testTreeEquality() { + BtreeP67 tree1 = BtreeP67.fromString("a(b,c)"); + BtreeP67 tree2 = BtreeP67.fromString("a(b,c)"); + BtreeP67 tree3 = BtreeP67.fromString("a(b,d)"); + BtreeP67 tree4 = BtreeP67.fromString("a(c,b)"); + BtreeP67 tree5 = BtreeP67.fromString("x(b,c)"); + BtreeP67 tree6 = BtreeP67.fromString("a(b(c,),)"); + + assertEquals(tree1, tree2, "Trees from identical strings should be equal"); + assertEquals(tree1.hashCode(), tree2.hashCode(), "Hash codes for equal trees should be equal"); + + assertNotEquals(tree1, tree3); + assertNotEquals(tree1, tree4); + assertNotEquals(tree1, tree5); + assertNotEquals(tree1, tree6); + assertNotEquals(tree1, null); + assertNotEquals(tree1, new Object()); + + BtreeP67 emptyTree1 = BtreeP67.fromString(""); + BtreeP67 emptyTree2 = new BtreeP67<>(null); + assertEquals(emptyTree1, emptyTree2); + assertEquals(emptyTree1.hashCode(), emptyTree2.hashCode()); + assertNotEquals(tree1, emptyTree1); + } + + @Test + void testFromStringSpecificCasesAndStructure() { + BtreeP67 treeAB = BtreeP67.fromString("a(b,)"); + assertNotNull(treeAB.getRoot()); + assertEquals("a", treeAB.getRoot().value); + assertNotNull(treeAB.getRoot().left); + assertEquals("b", treeAB.getRoot().left.value); + assertNull(treeAB.getRoot().right); + assertTrue(isLeaf(treeAB.getRoot().left)); + + BtreeP67 treeAC = BtreeP67.fromString("a(,c)"); + assertNotNull(treeAC.getRoot()); + assertEquals("a", treeAC.getRoot().value); + assertNull(treeAC.getRoot().left); + assertNotNull(treeAC.getRoot().right); + assertEquals("c", treeAC.getRoot().right.value); + assertTrue(isLeaf(treeAC.getRoot().right)); + + BtreeP67 treeXYZ = BtreeP67.fromString("x(y,z)"); + assertNotNull(treeXYZ.getRoot()); + assertEquals("x", treeXYZ.getRoot().value); + assertNotNull(treeXYZ.getRoot().left); + assertEquals("y", treeXYZ.getRoot().left.value); + assertTrue(isLeaf(treeXYZ.getRoot().left)); + assertNotNull(treeXYZ.getRoot().right); + assertEquals("z", treeXYZ.getRoot().right.value); + assertTrue(isLeaf(treeXYZ.getRoot().right)); + } +}