diff --git a/.gitignore b/.gitignore index b817f2d..e8feb05 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ .idea/**/dictionaries .idea/**/shelf -# Sensitive or high-churn files +# Sensitive or depth-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml diff --git a/README.md b/README.md index 5d9e4b9..5c597ce 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,29 @@ This project following the hexagonal architecture : - the package `domain` contains the business logic and the business objects. +## Efficiency of operations +- `N`ew order in the book : `O(log(book_depth))` +- `U`pdate an order in the book : `O(log(book_depth))` +- `D`elete an order in the book : `O(log(book_depth))` + +Overall, the complexity of the algorithm is `O(n * log(book_depth))` where `n` is the number of operations. + + +### Data structure: immutable indexed AVL Tree +#### Description +To have this this complexity, the data structure used is an immutable AVL tree (self-balanced tree). The tree is transformed to be able to have this interface : +```scala +sealed trait AVLIndexedTree[+T] { + def insert[U >: T](index: Int, elementToInsert: U): AVLIndexedTree[U] + def delete[U >: T](index: Int): AVLIndexedTree[U] + def updated[U >: T](index: Int, elementToInsert: U): AVLIndexedTree[U] +} +``` +This interface allows to insert, delete and update nodes of the tree using indexes instead of values. + +#### How is works ? +Every node know the number of children in the left and the right side. This enable to know the which side to choose for a given index. + ## Usage ### Run @@ -38,8 +61,6 @@ Where n is the book depth and price is in $. sbt test ``` -## Complexity -The complexity of the algorithm itself is `O(|updateOrderBookCommands| + book_depth)`. The parser part could probably be improved to be quicker. ## Example @@ -54,13 +75,10 @@ U B 1 5 40 ``` ``` -sbt "run lot-of-updates.txt 10.0 100" +sbt "run lot-of-updates.txt 10.0 2" ``` - Output ``` 50.0,40,60.0,10 40.0,40,70.0,20 ``` -- Visually - -![Visual example](./images/example.png) \ No newline at end of file diff --git a/src/main/scala/order/book/domain/OrderBook.scala b/src/main/scala/order/book/domain/OrderBook.scala index 6e7cc03..648dced 100644 --- a/src/main/scala/order/book/domain/OrderBook.scala +++ b/src/main/scala/order/book/domain/OrderBook.scala @@ -10,7 +10,6 @@ import scala.util.Try case class OrderBook private[OrderBook] (bids: OrderBookSide, asks: OrderBookSide) { - // O(1) def applyChange(changeRequest: UpdateOrderBookCommand): Try[OrderBook] = updateSide(changeRequest.side)(_.applyOrderChange( changeRequest.instruction, @@ -26,17 +25,17 @@ case class OrderBook private[OrderBook] (bids: OrderBookSide, asks: OrderBookSid } - // O(book_depth) - def project(tickSize: TickSize) : List[OrderBookProjection] = - bids.orders.zip(asks.orders) + def project(bookSize: Int, tickSize: TickSize) : List[OrderBookProjection] = + (0 until bookSize) + .zipAll(bids.getOrders, 0, EmptyOrder).map(_._2) + .zipAll(asks.getOrders, EmptyOrder, EmptyOrder) .map((OrderBookProjection.fromOrders(tickSize) _).tupled).toList } object OrderBook { - def apply(bookDepth: Int): OrderBook = - new OrderBook(OrderBookSide(bookDepth), OrderBookSide(bookDepth)) + def empty: OrderBook = new OrderBook(OrderBookSide.empty, OrderBookSide.empty) } diff --git a/src/main/scala/order/book/domain/OrderBookComputerImplementation.scala b/src/main/scala/order/book/domain/OrderBookComputerImplementation.scala index 80c7972..666c291 100644 --- a/src/main/scala/order/book/domain/OrderBookComputerImplementation.scala +++ b/src/main/scala/order/book/domain/OrderBookComputerImplementation.scala @@ -8,14 +8,13 @@ import scala.util.Try class OrderBookComputerImplementation extends OrderBookComputer { - // O(|updateOrderBookCommands| + book_depth) def compute(updateOrderBookCommands: Iterator[UpdateOrderBookCommand], tickSize: TickSize, bookDepth: Int): Try[List[OrderBookProjection]] = updateOrderBookCommands .filter(_.priceLevelIndex <= bookDepth) - .foldLeft(Try(OrderBook(bookDepth))){ + .foldLeft(Try(OrderBook.empty)){ case (bookTry, order) => bookTry.flatMap(_.applyChange(order)) } - .map(_.project(tickSize)) + .map(_.project(bookDepth, tickSize)) } diff --git a/src/main/scala/order/book/domain/OrderBookSide.scala b/src/main/scala/order/book/domain/OrderBookSide.scala index 8cc4490..83d953d 100644 --- a/src/main/scala/order/book/domain/OrderBookSide.scala +++ b/src/main/scala/order/book/domain/OrderBookSide.scala @@ -2,54 +2,28 @@ package order.book.domain import order.book.domain.commands.OrderBookInstruction import order.book.domain.commands.OrderBookInstruction.{Delete, New, Update} -import order.book.domain.errors.{CannotDeleteEmptyOrderError, UpdatingNonExistingValueError} +import order.book.domain.datastructure.AVLIndexedTree -import scala.util.{Failure, Success, Try} +import scala.util.{Success, Try} -case class OrderBookSide private[OrderBookSide] (orders: Vector[OrderBookOrder]) { +class OrderBookSide private[OrderBookSide] (orders: AVLIndexedTree[OrderBookOrder]) { - // O(1) def applyOrderChange(instruction: OrderBookInstruction, priceLevelIndex: Int, price: TickPrice, quantity: Quantity): Try[OrderBookSide] = { - - val updateOrdersIndexed = updateOrders(priceLevelIndex)(_) - - instruction match { - case New => - updateOrdersIndexed{ - case order: Order => Success(order.plus(quantity)) - case EmptyOrder => Success(EmptyOrder.withPrice(price).plus(quantity)) - } - - case Update => - updateOrdersIndexed{ - case order: Order => Success(order.replace(quantity)) - case EmptyOrder => Failure(new UpdatingNonExistingValueError(priceLevelIndex, price, quantity)) - } - - case Delete => - updateOrdersIndexed{ - case order: Order => order.minus(quantity) - case EmptyOrder => Failure(new CannotDeleteEmptyOrderError(priceLevelIndex, price, quantity)) - } - } + val zeroStartingIndex = priceLevelIndex - 1 + Success(instruction match { + case New => new OrderBookSide(orders.insert(zeroStartingIndex, Order(price, quantity))) + case Update => new OrderBookSide(orders.updated(zeroStartingIndex, Order(price, quantity))) + case Delete => new OrderBookSide(orders.delete(zeroStartingIndex)) + }) } + def getOrders: List[OrderBookOrder] = orders.toList - // O(updateOrders) = O(updateOrder) - private def updateOrders(priceLevelIndex: Int)(updateOrder: OrderBookOrder => Try[OrderBookOrder]): Try[OrderBookSide] = { - val vectorIndex = priceLevelIndex - 1 - - for{ - orderToUpdate <- Try(orders(vectorIndex)) // O(1) - newOrder <- updateOrder(orderToUpdate) - ordersUpdated <- Try(orders.updated(vectorIndex, newOrder)) // O(1) - } yield copy(orders = ordersUpdated) - } } object OrderBookSide { - def apply(bookDepth: Int): OrderBookSide = new OrderBookSide(Vector.fill(bookDepth)(EmptyOrder)) + def empty: OrderBookSide = new OrderBookSide(AVLIndexedTree.empty) } \ No newline at end of file diff --git a/src/main/scala/order/book/domain/datastructure/AVLIndexedTree.scala b/src/main/scala/order/book/domain/datastructure/AVLIndexedTree.scala new file mode 100644 index 0000000..00a877a --- /dev/null +++ b/src/main/scala/order/book/domain/datastructure/AVLIndexedTree.scala @@ -0,0 +1,239 @@ +package order.book.domain.datastructure + + +sealed trait AVLIndexedTree[+T] { + val depth: Int + val level: Int + val balanceScore: Int + def insert[U >: T](index: Int, elementToInsert: U): AVLIndexedTree[U] + def delete[U >: T](index: Int): AVLIndexedTree[U] + def updated[U >: T](index: Int, elementToInsert: U): AVLIndexedTree[U] + def balance: AVLIndexedTree[T] + def toList: List[T] +} + +object AVLIndexedTree { + def empty[T]: AVLIndexedTree[T] = EmptyTree +} + +case object EmptyTree extends AVLIndexedTree[Nothing] { + val depth: Int = 0 + val level: Int = 0 + val balanceScore: Int = 0 + + def insert[T](index: Int, elementToInsert: T): AVLIndexedTree[T] = + Node(EmptyTree, 0, elementToInsert, 0, EmptyTree) + + def delete[T](index: Int): AVLIndexedTree[T] = EmptyTree + + def updated[T](index: Int, elementToInsert: T): AVLIndexedTree[T] = EmptyTree + + override def toList: List[Nothing] = List.empty + + override def balance: AVLIndexedTree[Nothing] = this +} + +case class Node[T](left: AVLIndexedTree[T], leftNumberOfElement: Int, + element: T, + rightNumberOfElement: Int, right: AVLIndexedTree[T]) extends AVLIndexedTree[T] { + + + def isCurrentNode(index: Int): Boolean = leftNumberOfElement == index + def isInTheLeftSide(index: Int): Boolean = index < leftNumberOfElement + def isInTheRightSide(index: Int): Boolean = index > leftNumberOfElement + + def computeLeftIndex(index: Int): Int = index + def computeRightIndex(index: Int): Int = index - leftNumberOfElement - 1 + + override def balance: AVLIndexedTree[T] = { + + def leftRotation(tree: AVLIndexedTree[T]): AVLIndexedTree[T] = this match { + case Node(_, _, _, _, Node(rightLeft, rightLeftNumberOfElement, rightElement, rightRightNumberOfElement, rightRight)) => + Node( + Node( + left, + leftNumberOfElement, + element, + rightNumberOfElement - rightRightNumberOfElement - 1, + rightLeft + ), + rightLeftNumberOfElement + leftNumberOfElement + 1, + rightElement, + rightRightNumberOfElement, + rightRight + ) + case _ => this + } + + def rightLeftRotation(tree: AVLIndexedTree[T]): AVLIndexedTree[T] = this match { + case Node(_, _, _, _, + Node( + Node( + rightLeftLeft, + rightLeftLeftNumberOfElement, + rightLeftElement, + rightLeftRightNumberOfElement, + rightLeftRight + ), + rightLeftNumberOfElement, + rightElement, + rightRightNumberOfElement, + rightRight + ) + ) => + Node( + Node( + left, + leftNumberOfElement, + element, + rightNumberOfElement - rightRightNumberOfElement - rightLeftRightNumberOfElement - 2, + rightLeftLeft + ), + rightLeftLeftNumberOfElement + leftNumberOfElement + 1, + rightLeftElement, + rightRightNumberOfElement + rightLeftRightNumberOfElement + 1, + Node( + rightLeftRight, + rightLeftNumberOfElement - rightLeftLeftNumberOfElement - 1, + rightElement, + rightRightNumberOfElement, + rightRight + ) + ) + case _ => this + } + + def rightRotation(tree: AVLIndexedTree[T]): AVLIndexedTree[T] = this match { + case Node(Node(leftLeft, leftLeftNumberOfElement, leftElement, leftRightNumberOfElement, leftRight), _, _, _, _) => + Node( + leftLeft, + leftLeftNumberOfElement, + leftElement, + leftRightNumberOfElement + rightNumberOfElement + 1, + Node( + leftRight, + leftNumberOfElement - leftLeftNumberOfElement - 1, + element, + rightNumberOfElement, + right + ) + ) + case _ => this + } + + def leftRightRotation(tree: AVLIndexedTree[T]): AVLIndexedTree[T] = this match { + case Node( + Node( + leftLeft, + leftLeftNumberOfElement, + leftElement, + leftRightNumberOfElement, + Node( + leftRightLeft, + leftRightLeftNumberOfElement, + leftRightElement, + leftRightRightNumberOfElement, + leftRightRight + ) + ), _, _, _, _) => + Node( + Node( + leftLeft, + leftLeftNumberOfElement, + leftElement, + leftRightNumberOfElement - leftRightRightNumberOfElement - 1, + leftRightLeft + ), + leftRightLeftNumberOfElement + leftLeftNumberOfElement + 1, + leftRightElement, + leftRightRightNumberOfElement + rightNumberOfElement + 1, + Node( + leftRightRight, + leftNumberOfElement - leftLeftNumberOfElement - leftRightLeftNumberOfElement - 2, + element, + rightNumberOfElement, + right + ) + ) + case _ => this + } + + if(balanceScore == -2){ + if(left.balanceScore == -1) rightRotation(this) + else leftRightRotation(this) + }else if(balanceScore == 2){ + if(right.balanceScore == 1) leftRotation(this) + else rightLeftRotation(this) + }else{ + this + } + } + + override def insert[U >: T](index: Int, elementToInsert: U): AVLIndexedTree[U] = + if(isInTheLeftSide(index) || isCurrentNode(index)){ + copy( + left = left.insert(computeLeftIndex(index), elementToInsert).balance, + leftNumberOfElement + 1 + ).balance + }else{ + copy( + right = right.insert(computeRightIndex(index), elementToInsert).balance, + rightNumberOfElement = rightNumberOfElement + 1 + ).balance + } + + def deleteMin: (T, AVLIndexedTree[T]) = this match { + case Node(EmptyTree, _, _, _, _) => (element, EmptyTree) + case Node(leftNode @ Node(_, _, _, _, _), _, _, _, _) => leftNode.deleteMin match { + case (minElement, leftTree) => (minElement, copy(left = leftTree, leftNumberOfElement = leftNumberOfElement - 1)) + } + } + + override def delete[U >: T](index: Int): AVLIndexedTree[U] = this match { + case _ if isInTheLeftSide(index) => + copy( + left = left.delete(computeLeftIndex(index)).balance, + leftNumberOfElement = leftNumberOfElement - 1 + ).balance + case _ if isInTheRightSide(index) => + copy( + right = right.delete(computeRightIndex(index)).balance, + rightNumberOfElement = rightNumberOfElement - 1 + ).balance + + // Case one - no children + case Node(EmptyTree, _, _, _, EmptyTree) => EmptyTree + + // Case two - one child + case Node(Node(_, _, _, _, _), _, _, _, EmptyTree) => left + case Node(EmptyTree, _, _, _, Node(_, _,_, _, _)) => right + + // Case tree - two children + case Node(_, _, _, _, rightNode @ Node(_, _, _, _, _)) => + rightNode.deleteMin match { + case (minElement, rightTree) => Node(left, leftNumberOfElement, minElement, rightNumberOfElement - 1, rightTree) + } + } + + + + override def updated[U >: T](index: Int, elementToInsert: U): AVLIndexedTree[U] = + if(isCurrentNode(index)) copy(element = elementToInsert) + else if (isInTheLeftSide(index)) copy(left = left.updated(computeLeftIndex(index), elementToInsert)) + else copy(right = right.updated(computeRightIndex(index), elementToInsert)) + + + override def toList: List[T] = left.toList ++ (element +: right.toList) + + lazy val depth: Int = this match { + case Node(EmptyTree, _, _, _, EmptyTree) => 0 + case _ => 1 + Math.max(left.depth, right.depth) + } + + lazy val level: Int = depth + 1 + + override val balanceScore: Int = right.level - left.level +} + + + diff --git a/src/test/scala/order/book/domain/OrderBookSideSpec.scala b/src/test/scala/order/book/domain/OrderBookSideSpec.scala index 217b248..5ce7e29 100644 --- a/src/test/scala/order/book/domain/OrderBookSideSpec.scala +++ b/src/test/scala/order/book/domain/OrderBookSideSpec.scala @@ -7,18 +7,21 @@ class OrderBookSideSpec extends WordSpec with Matchers { "It" should { "be possible to add an order in a side of the book" in { - OrderBookSide(1).applyOrderChange( - New, - 1, - TickPrice(5), - Quantity(30) - ).get.orders should be (Vector( - Order(TickPrice(5), Quantity(30)) - )) + OrderBookSide + .empty + .applyOrderChange( + New, + 1, + TickPrice(5), + Quantity(30) + ).get.getOrders should be (List( + Order(TickPrice(5), Quantity(30)) + )) } "be possible to update an order in a side of the book" in { - OrderBookSide(1) + OrderBookSide + .empty .applyOrderChange( New, 1, @@ -30,13 +33,14 @@ class OrderBookSideSpec extends WordSpec with Matchers { 1, TickPrice(5), Quantity(40) - ).get.orders should be (Vector( + ).get.getOrders should be (List( Order(TickPrice(5), Quantity(40)) )) } "be possible to delete an order in a side of the book" in { - OrderBookSide(1) + OrderBookSide + .empty .applyOrderChange( New, 1, @@ -48,13 +52,12 @@ class OrderBookSideSpec extends WordSpec with Matchers { 1, TickPrice(5), Quantity(10) - ).get.orders should be (Vector( - Order(TickPrice(5), Quantity(20)) - )) + ).get.getOrders should be (List.empty) } - "be possible to do several actions and get the excepted projections" in { - OrderBookSide(2) + "be possible to do several actions and get the excepted projections 1" in { + OrderBookSide + .empty .applyOrderChange( New, 1, @@ -72,7 +75,7 @@ class OrderBookSideSpec extends WordSpec with Matchers { 1, TickPrice(5), Quantity(40) - ).get.orders should be (Vector( + ).get.getOrders should be (List( Order(TickPrice(5), Quantity(40)), Order(TickPrice(4), Quantity(40)) )) diff --git a/src/test/scala/order/book/domain/OrderBookSpec.scala b/src/test/scala/order/book/domain/OrderBookSpec.scala index d70ccfc..9ce2e96 100644 --- a/src/test/scala/order/book/domain/OrderBookSpec.scala +++ b/src/test/scala/order/book/domain/OrderBookSpec.scala @@ -11,7 +11,7 @@ class OrderBookSpec extends WordSpec with Matchers { "It" should { "be possible to apply a change to the bid size" in { - val book = OrderBook(1) + val book = OrderBook.empty .applyChange(UpdateOrderBookCommand( New, Bid, @@ -20,12 +20,12 @@ class OrderBookSpec extends WordSpec with Matchers { Quantity(30) )) - book.get.asks.orders should be (Vector(EmptyOrder)) - book.get.bids.orders should be (Vector(Order(TickPrice(5), Quantity(30)))) + book.get.asks.getOrders should be (List.empty) + book.get.bids.getOrders should be (List(Order(TickPrice(5), Quantity(30)))) } "be possible to apply a change to the ask size" in { - val book = OrderBook(1) + val book = OrderBook.empty .applyChange(UpdateOrderBookCommand( New, Ask, @@ -34,22 +34,55 @@ class OrderBookSpec extends WordSpec with Matchers { Quantity(30) )) - book.get.asks.orders should be (Vector(Order(TickPrice(5), Quantity(30)))) - book.get.bids.orders should be (Vector(EmptyOrder)) + book.get.asks.getOrders should be (List(Order(TickPrice(5), Quantity(30)))) + book.get.bids.getOrders should be (List.empty) } "be possible to describe a book" in { - OrderBook(1) + OrderBook.empty .applyChange(UpdateOrderBookCommand( New, Ask, 1, TickPrice(5), Quantity(30) - )).get.project(TickSize(10)) should be (List( + )).get.project(1, TickSize(10)) should be (List( OrderBookProjection(0, Quantity(0), 50, Quantity(30)) )) } + + "work with a simple example" in { + + /* + INPUT: + N A 1 1 10 + N A 1 2 20 + + OUTPUT: + 0.0,0,20.0,20 + 0.0,0,10.0,10 + */ + + OrderBook.empty + .applyChange(UpdateOrderBookCommand( + New, + Ask, + 1, + TickPrice(1), + Quantity(10) + )).get + .applyChange(UpdateOrderBookCommand( + New, + Ask, + 1, + TickPrice(2), + Quantity(20) + )) + .get.project(1, TickSize(10)) should be (List( + OrderBookProjection(0, Quantity(0), 20, Quantity(20)), + OrderBookProjection(0, Quantity(0), 10, Quantity(10)) + )) + } } } diff --git a/src/test/scala/order/book/domain/datastructure/AVLIndexedTreeSpec.scala b/src/test/scala/order/book/domain/datastructure/AVLIndexedTreeSpec.scala new file mode 100644 index 0000000..e57e8ef --- /dev/null +++ b/src/test/scala/order/book/domain/datastructure/AVLIndexedTreeSpec.scala @@ -0,0 +1,245 @@ +package order.book.domain.datastructure + +import org.scalatest.{Matchers, WordSpec} + + +class AVLIndexedTreeSpec extends WordSpec with Matchers { + + "It" should { + "be possible to know if a given index is in the left or the right side of the tree" in { + val node = Node(EmptyTree, 3, '5', 5, EmptyTree) + + node.isInTheLeftSide(2) should be (true) + node.isInTheLeftSide(5) should be (false) + } + + "be possible to turn the tree into a List" in { + EmptyTree + .insert(0, '1') + .insert(1, '2') + .insert(3, '3') + .insert(4, '4') + .insert(5, '5') + .toList should be ( + List('1', '2', '3', '4', '5') + ) + } + } + + "An value" should { + "be addable to an empty tree" in { + EmptyTree.insert(1, '1').toList should be (List('1')) + } + "be added before with the same id" in { + EmptyTree + .insert(0, '5') + .insert(0, '1') should be ( + Node(Node(EmptyTree,0,'1',0,EmptyTree),1,'5',0,EmptyTree) + ) + } + "be added after with the same id should " in { + EmptyTree + .insert(0, '5') + .insert(1, '1') should be ( + Node(EmptyTree,0,'5',1,Node(EmptyTree,0,'1',0,EmptyTree)) + ) + } + "be addable in the middle of the tree" in { + EmptyTree + .insert(0, '1') + .insert(1, '2') + .insert(4, '4') + .insert(5, '5') + .insert(2, '3') + .toList should be ( + List('1', '2', '3', '4', '5') + ) + } + } + + "The depth of the tree" should { + "be 0" in { + EmptyTree.depth should be (0) + Node(EmptyTree, 0, '1', 0, EmptyTree).depth should be (0) + } + "be 1" in { + Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 0, EmptyTree).depth should be (1) + Node(EmptyTree, 0, '1', 2, Node(EmptyTree, 0, '2', 1, EmptyTree)).depth should be (1) + } + "be 2" in { + Node(Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 0, EmptyTree), 2, '3', 0, EmptyTree).depth should be (2) + Node(EmptyTree, 0, '1', 2, Node(Node(EmptyTree, 0, '2', 0, EmptyTree), 1, '3', 0, EmptyTree)).depth should be (2) + } + } + + "The balance score" should { + "be 0" in { + EmptyTree.balanceScore should be (0) + Node(EmptyTree, 0, '2', 1, EmptyTree).balanceScore should be (0) + } + "be 1" in { + Node(EmptyTree, 0, '1', 2, Node(EmptyTree, 0, '2', 1, EmptyTree)).balanceScore should be (1) + } + "be -1" in { + Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 0, EmptyTree).balanceScore should be (-1) + } + "be 2 (1)" in { + Node(EmptyTree, 0, '1', 2, Node(EmptyTree, 0, '2', 1, Node(EmptyTree, 0, '3', 0, EmptyTree))).balanceScore should be (2) + } + "be 2 (2)" in { + Node(EmptyTree, 0, '1', 2, Node(Node(EmptyTree, 0, '2', 0, EmptyTree), 1, '3', 0, EmptyTree)).balanceScore should be (2) + } + "be -2 (1)" in { + Node(Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 0, EmptyTree), 2, '3', 0, EmptyTree).balanceScore should be (-2) + } + "be -2 (2)" in { + Node(Node(EmptyTree, 0, '2', 1, Node(EmptyTree, 0, '1', 0, EmptyTree)), 2, '3', 0, EmptyTree).balanceScore should be (-2) + } + } + + "A tree" should { + "be balanced 1" in { + Node(EmptyTree, 0, '1', 2, Node(EmptyTree, 0, '2', 1, Node(EmptyTree, 0, '3', 0, EmptyTree))) + .balance should be ( + Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 1, Node(EmptyTree, 0, '3', 0, EmptyTree)) + ) + } + "be balanced 2" in { + Node(EmptyTree, 0, '1', 2, Node(Node(EmptyTree, 0, '2', 0, EmptyTree), 1, '3', 0, EmptyTree)) + .balance should be ( + Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 1, Node(EmptyTree, 0, '3', 0, EmptyTree)) + ) + } + "be balanced 3" in { + Node(Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 0, EmptyTree), 2, '3', 0, EmptyTree) + .balance should be ( + Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 1, Node(EmptyTree, 0, '3', 0, EmptyTree)) + ) + } + "be balanced 4" in { + Node(Node(EmptyTree, 0, '1', 1, Node(EmptyTree, 0, '2', 0, EmptyTree)), 2, '3', 0, EmptyTree) + .balance should be ( + Node(Node(EmptyTree, 0, '1', 0, EmptyTree), 1, '2', 1, Node(EmptyTree, 0, '3', 0, EmptyTree)) + ) + } + } + + "A leaf of the tree" should { + "be deletable if this element is a leaf (without children)" in { + + EmptyTree + .insert(0, '1') + .insert(1, '2') + .delete(1) should be ( + Node(EmptyTree, 0, '1', 0, EmptyTree) + ) + + + EmptyTree + .insert(0, '1') + .insert(1, '2') + .insert(3, '3') + .insert(4, '4') + .insert(5, '5') + .delete(2) should be ( + Node( + Node(EmptyTree,0,'1',0,EmptyTree), + 1,'2',2, + Node(EmptyTree,0,'4',1, + Node(EmptyTree,0,'5',0,EmptyTree) + ) + ) + ) + } + "if this element is a leaf (with one child) - 1" in { + EmptyTree + .insert(0, '1') + .insert(1, '2') + .insert(3, '3') + .insert(4, '4') + .insert(5, '5') + .delete(0) + .toList should be ( + List('2', '3', '4', '5') + ) + } + + "if this element is a leaf (with two children)" in { + EmptyTree + .insert(0, '1') + .insert(1, '2') + .insert(3, '3') + .insert(4, '4') + .insert(5, '5') + .delete(3) + .toList should be( + List('1', '2', '3', '5') + ) + } + } + + + "It" should { + "be possible to update a simple tree" in { + EmptyTree + .insert(0, '1') + .updated(0, 'a') + .toList should be ( + List('a') + ) + + EmptyTree + .insert(0, '1') + .insert(1, '2') + .updated(1, 'a') + .toList should be ( + List('1', 'a') + ) + EmptyTree + .insert(0, '2') + .insert(0, '1') + .updated(0, 'a') + .toList should be ( + List('a', '2') + ) + } + "be possible to update a value in the tree - 1" in { + EmptyTree + .insert(0, '1') + .insert(1, '2') + .insert(3, '3') + .insert(4, '4') + .insert(5, '5') + .updated(0, 'a') + .toList should be ( + List('a', '2', '3', '4', '5') + ) + } + "be possible to update a value in the tree - 2" in { + EmptyTree + .insert(0, '1') + .insert(1, '2') + .insert(3, '3') + .insert(4, '4') + .insert(5, '5') + .updated(2, 'a') + .toList should be ( + List('1', '2', 'a', '4', '5') + ) + } + "be possible to update a value in the tree - 3" in { + EmptyTree + .insert(0, '1') + .insert(1, '2') + .insert(3, '3') + .insert(4, '4') + .insert(5, '5') + .updated(4, 'a') + .toList should be ( + List('1', '2', '3', '4', 'a') + ) + } + } + + +}