Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions src/main/java/org/nintynine/problems/BtreeP67.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package org.nintynine.problems;

import java.util.Objects; // For Objects.equals and Objects.hash

public class BtreeP67<T> {

static class Node<T> { // Changed from private static to static (package-private)
T value;
Node<T> left;
Node<T> 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<T> root;

public BtreeP67() {
this.root = null;
}

// Constructor to create a tree with a given root.
public BtreeP67(Node<T> root) {
this.root = root;
}

// Getter for the root, might be useful for testing.
public Node<T> getRoot() {
return root;
}

public static BtreeP67<String> 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<String> 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<String> 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<String> 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<T> 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);
}
}
223 changes: 223 additions & 0 deletions src/test/java/org/nintynine/problems/BtreeP67Test.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> tree = new BtreeP67<>(root);
assertEquals("a(b(d,e),c(,f(g,)))", tree.toString());
}

@Test
void testFromString_singleNode() {
BtreeP67<String> 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<String> tree = BtreeP67.fromString(s);
assertNotNull(tree.getRoot(), "Root for 'a(b(c,),)' should not be null");
assertEquals("a", tree.getRoot().value);

BtreeP67.Node<String> 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<String> 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<String> tree = BtreeP67.fromString(s);
String roundTripString = tree.toString();
assertEquals(s, roundTripString, "Round trip toString(fromString(s)) failed for: " + s);

BtreeP67<String> treeFromRoundTripString = BtreeP67.fromString(roundTripString);
assertEquals(tree, treeFromRoundTripString, "Round trip fromString(toString(tree)) failed for: " + s);
}

BtreeP67<String> treeFromVerboseLeaf = BtreeP67.fromString("z(,)");
assertEquals("z", treeFromVerboseLeaf.toString(), "Tree from 'z(,)' should stringify to 'z'");
BtreeP67<String> treeFromSimpleLeaf = BtreeP67.fromString("z");
assertEquals(treeFromSimpleLeaf, treeFromVerboseLeaf, "Tree from 'z(,)' should be equal to tree from 'z'");
}

@Test
void testToStringForManuallyConstructedTrees() {
BtreeP67<String> leaf = new BtreeP67<>(new BtreeP67.Node<>("x"));
assertEquals("x", leaf.toString());

BtreeP67.Node<String> rootLeft = new BtreeP67.Node<>("p");
rootLeft.left = new BtreeP67.Node<>("q");
BtreeP67<String> treeLeft = new BtreeP67<>(rootLeft);
assertEquals("p(q,)", treeLeft.toString());

BtreeP67.Node<String> rootRight = new BtreeP67.Node<>("m");
rootRight.right = new BtreeP67.Node<>("n");
BtreeP67<String> treeRight = new BtreeP67<>(rootRight);
assertEquals("m(,n)", treeRight.toString());

BtreeP67.Node<String> rootTwo = new BtreeP67.Node<>("k");
rootTwo.left = new BtreeP67.Node<>("l");
rootTwo.right = new BtreeP67.Node<>("m");
BtreeP67<String> treeTwo = new BtreeP67<>(rootTwo);
assertEquals("k(l,m)", treeTwo.toString());

BtreeP67.Node<String> rootWithEmptyChildren = new BtreeP67.Node<>("j");
BtreeP67<String> treeEmptyChildren = new BtreeP67<>(rootWithEmptyChildren);
assertEquals("j", treeEmptyChildren.toString());
}

@Test
void testFromString_emptyString() {
BtreeP67<String> 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<String> tree1 = BtreeP67.fromString("a(b,c)");
BtreeP67<String> tree2 = BtreeP67.fromString("a(b,c)");
BtreeP67<String> tree3 = BtreeP67.fromString("a(b,d)");
BtreeP67<String> tree4 = BtreeP67.fromString("a(c,b)");
BtreeP67<String> tree5 = BtreeP67.fromString("x(b,c)");
BtreeP67<String> 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<String> emptyTree1 = BtreeP67.fromString("");
BtreeP67<String> emptyTree2 = new BtreeP67<>(null);
assertEquals(emptyTree1, emptyTree2);
assertEquals(emptyTree1.hashCode(), emptyTree2.hashCode());
assertNotEquals(tree1, emptyTree1);
}

@Test
void testFromStringSpecificCasesAndStructure() {
BtreeP67<String> 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<String> 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<String> 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));
}
}