diff --git a/.gitignore b/.gitignore index 13b5013..1c85e7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,33 @@ /.idea/* *.iml /target/* +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + diff --git a/README.md b/README.md index 445961c..bb81d1c 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,74 @@ # tree_printer + A Java class for printing binary trees as ASCII text -It hasn't been optimized for run time efficiency, but since we're talking about printing in ASCII, I figured it's not going to be used on very large trees. It does have some nice features though. +It hasn't been optimized for run time efficiency, but since we're talking about printing in ASCII, I figured it's not going to be used on very large trees. It does have some nice features though. + +1. It makes efficient use of space in that a large subtree extends under a smaller one as much as possible. +2. It's generic, working for any binary tree data objects, as long as you can provide functions (lambda functions will do) to get a nodes label as a String, and to get the left and right sub-nodes. +3. There's a parameter to set the minimum horizontal space between node labels. +4. Node labels are strings of arbitrary length. +5. In addition to a method for printing a single tree, there's a method for printing a list of trees horizontally across the page (with a parameter for page width), using as many rows as necessary. +6. There's an option to print trees with diagonal branches (diagonal unicode box drawing characters) or with horizontal branches (using unicode box drawing characters). The latter is more compact and makes tree levels more visually clear. +7. It supports basic ANSI escape sequences for colored output to terminal. +8. It works. + +Some [demo/test programs](src/test/java) are included. - 1. It makes efficient use of space in that a large subtree extends under a smaller one as much as possible. - 2. It's generic, working for any binary tree data objects, as long as you can provide functions (lambda functions will do) to get a nodes label as a String, and to get the left and right sub-nodes. - 3. There's a parameter to set the minimum horizontal space between node labels. - 4. Node labels are strings of arbitrary length. - 5. In addition to a method for printing a single tree, there's a method for printing a list of trees horizontally across the page (with a parameter for page width), using as many rows as necessary. - 6. There's an option to print trees with diagonal branches (using slash and backslash characters) or with horizontal branches (using ascii box drawing characters). The latter is more compact and makes tree levels more visually clear. - 7. It works. +## Usage -Some demo/test programs are included. +### Gradle: -The TreePrinter object has two methods for printing binary trees as ASCII text. PrintTree prints a single tree. PrintTrees prints a list of trees horizontally across the page, in multiple rows if necessary. +Use `sourceControl` block in your `settings.gradle.kts` -The TreePrinter object has a few settable parameters affecting how it prints trees. A positive integer parameter 'hspace' specifies the minimum number of horizontal spaces between any two node labels in the tree. A boolean parameter 'squareBranches' determines whether the tree is drawn with horizontal branches (using ascii box drawing characters) or diagonal branches (using slash and backslash characters). The boolean 'lrAgnostic' parameter only affects trees drawn with the ascii box drawing characters. Its effect is is that tree nodes with only a single subtree are drawn with a straight down vertical branch, providing no indication of whether it is a left or right subtree. +```kotlin +// in settings.gradle.kts: +sourceControl { + // without the `uri(...)` if you use Groovy + gitRepository( + uri("https://github.com/billvanyo/tree_printer.git") + ) { + producesModule( + "tech.vanyo:tree_printer:1.1" + ) + } +} +``` -A few test/demo programs are included. For instance, the program EnumTrees can be used to print an enumeration of all binary trees of a given size. All trees of size 5, labeled with number words (one, two, etc) is printed as: +and add the dependency in your buildscript `build.gradle.kts` dependencies block: +```kotlin +dependencies { + implementation( + "tech.vanyo:tree_printer:1.1" + ) +} ``` -mvn compile -mvn exec:java -Dexec.mainClass="EnumTrees" + +## Details + +The TreePrinter object has two methods for printing binary trees as ASCII text. `printTree(tree)` prints a single tree. +`printTrees(trees, lineWidth)` prints a list of trees horizontally across the page, in multiple rows if necessary. + +The TreePrinter object has a few settable parameters affecting how it prints trees: + +- A positive integer parameter `labelGap` specifies the minimum number of horizontal spaces between any two node labels in the tree. +- `colGap` and `rowGap` specifies spacing between trees when using `printTrees(trees, lineWidth)` +- A boolean parameter `squareBranches` determines whether the tree is drawn with horizontal branches (using ascii box drawing characters) or diagonal branches (using slash and backslash characters). +- The boolean `lrAgnostic` parameter only affects trees drawn with square style. Its effect is that tree nodes with only a single subtree are drawn with a straight down vertical branch, providing no indication of whether it is a left or right subtree. +- `usePlaceholder` replaces empty labels with placeholders + +## Examples + +A few test/demo programs are included. For instance, the program EnumTrees can be used to print an enumeration of all binary trees of a given size. All trees of size 5, labeled with number words (one, two, etc) is printed as: + +```bash +# use gradlew.bat on windows +./gradlew :testLogging --tests *EnumTrees ``` + This produces output like: + ``` one one one one one one one one one \ \ \ \ \ \ \ \ \ @@ -81,14 +126,21 @@ one four two four one one two three three \ / one three \ / three two two one ``` -RandomTree can be used to print a single randomly generated tree. The following is an example of the same tree -printed 4 different ways, with horizontal spacing of 1 and of 3, and with diagonal and horizontal branches. To -run this from the command line using maven type: + +[RandomTree](src/test/java/RandomTree.java) can be used to print a single randomly generated tree. The following is an example of +the +same +tree +printed 4 different ways, with horizontal spacing of 1 and of 3, and with diagonal and horizontal branches. To +run this from the command line using maven type: + ``` -mvn compile -mvn exec:java -Dexec.mainClass="RandomTree" +# use gradlew.bat on windows +./gradlew :testLogging --tests *RandomTree ``` + This produces output like: + ``` 27 ┌─────┴─────┐ @@ -182,10 +234,17 @@ This produces output like: 14 16 ``` -There's a demo program that produces a tree diagram of all Collatz sequences -(https://en.wikipedia.org/wiki/Collatz_conjecture) of a given length. This demonstrates an option to print trees + +There's a [demo program](src/test/java/CollatzTree.java) that produces a tree diagram of all Collatz sequences +(https://en.wikipedia.org/wiki/Collatz_conjecture) of a given length. This demonstrates an option to print trees in such a way that if there is only a single subtree, it is treated the same regardless of whether it is a left or right subtree. This produces output like: + +```bash +# use gradlew.bat on windows +./gradlew :testLogging --tests *CollatzTree +``` + ``` 1 │ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..082a5bf --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,43 @@ +import org.gradle.api.tasks.testing.logging.TestLogEvent + +plugins { + java +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + testImplementation("org.junit.jupiter", "junit-jupiter-api", "5.8.1") + testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine", "5.1.8") +} + +group = "tech.vanyo" +version = "1.1" +description = "Print binary trees in the terminal neatly with minimal overhead. Small dependency." + +java { + toolchain { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +tasks.withType { + options.encoding = "UTF-8" +} + +tasks.register("testLogging") { + description = "Run test task with stdout/stderr logged" + group = "verification" + testLogging { + outputs.upToDateWhen { false } // don't cache this task's result + events = setOf(TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.STANDARD_OUT, TestLogEvent.STARTED) + } +} + +tasks.withType().configureEach { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index e697fdf..0000000 --- a/pom.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - 4.0.0 - - tech.vanyo.treePrinter - tree_printer - 1.0-SNAPSHOT - - - UTF-8 - 1.8 - 1.8 - - - - - junit - junit - 4.11 - test - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-jar-plugin - 3.0.2 - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - - \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..0e5afcd --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "tree_printer" diff --git a/src/main/java/RandomTree.java b/src/main/java/RandomTree.java deleted file mode 100644 index 340c49a..0000000 --- a/src/main/java/RandomTree.java +++ /dev/null @@ -1,76 +0,0 @@ -import tech.vanyo.treePrinter.TreePrinter; - -import java.util.Random; - -public class RandomTree { - - private static Random r = new Random(); - - public static void main(String[] args) { - TreeNode tree = randomTree(30); - - /* - We declare a TreePrinter object, parameterized with the type of tree object it will be printing (in this - case TreeNode), and call the TreePrinter constructor, providing lambda functions to get the TreeNode's - label as a String, and to get the left and right and right subtrees. - */ - TreePrinter printer = new TreePrinter<>(n -> nameForNumber(n.getValue()), n -> n.getLeft(), n -> n.getRight()); - // set minimum horizontal spacing between node labels with setHspace - printer.setHspace(1); - // use square branches - printer.setSquareBranches(true); - printer.printTree(tree); - System.out.println(); - - printer = new TreePrinter<>(n -> ""+n.getValue(), n -> n.getLeft(), n -> n.getRight()); - printer.setHspace(1); - // use square branches - printer.setSquareBranches(true); - printer.printTree(tree); - System.out.println(); - - // option to render single left or right subtree as straight down branch (i.e. no indication of left or right) - printer.setLrAgnostic(true); - printer.printTree(tree); - - // use diagonal branches - printer.setSquareBranches(false); - printer.printTree(tree); - System.out.println(); - - printer.setHspace(3); - printer.setSquareBranches(true); - printer.printTree(tree); - System.out.println(); - - printer.setSquareBranches(false); - printer.printTree(tree); - } - - public static TreeNode randomTree(int n) { - return randomTree(1, n); - } - - private static TreeNode randomTree(int firstValue, int lastValue) { - if (firstValue > lastValue) return null; - else { - int treeSize = lastValue - firstValue + 1; - int leftCount = r.nextInt(treeSize); - int rightCount = treeSize - leftCount - 1; - TreeNode root = new TreeNode(firstValue + leftCount); - root.setLeft(randomTree(firstValue, firstValue + leftCount - 1)); - root.setRight(randomTree(firstValue + leftCount + 1, lastValue)); - return root; - } - } - - private static String nameForNumber(int n) { - final String[] underTwenty = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", - "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"}; - final String[] decades = {"twenty", "thirty", "fourty", "fifty", "sixty", "seventy", "eighty", "ninety"}; - - if (n < 20) return underTwenty[n]; - else if (n > 99) return "" + n; // not implemented - else return decades[n / 10 - 2] + (n % 10 == 0 ? "" : (" " + underTwenty[n % 10])); - } -} diff --git a/src/main/java/TreeNode.java b/src/main/java/tech/vanyo/treePrinter/TreeNode.java similarity index 97% rename from src/main/java/TreeNode.java rename to src/main/java/tech/vanyo/treePrinter/TreeNode.java index 843515c..2775dc1 100644 --- a/src/main/java/TreeNode.java +++ b/src/main/java/tech/vanyo/treePrinter/TreeNode.java @@ -1,3 +1,5 @@ +package tech.vanyo.treePrinter; + public class TreeNode { private int value; private TreeNode left; diff --git a/src/main/java/tech/vanyo/treePrinter/TreePrinter.java b/src/main/java/tech/vanyo/treePrinter/TreePrinter.java index 736f093..e68b80c 100644 --- a/src/main/java/tech/vanyo/treePrinter/TreePrinter.java +++ b/src/main/java/tech/vanyo/treePrinter/TreePrinter.java @@ -1,61 +1,98 @@ package tech.vanyo.treePrinter; import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.function.Function; +import java.util.regex.Pattern; public class TreePrinter { + public static final Pattern ANSI_REGEX = Pattern.compile("\\e\\[[\\d;]*[^\\d;]"); + + /** Segments for drawing a tree. See documentation of their values for more info. */ + public enum Segment { + /**Diagonal placeholder*/ D_PLACEHOLDER, + /**Diagonal left*/ D_LEFT, + /**Diagonal right*/ D_RIGHT, + + /**Placeholder in square style*/ S_PLACEHOLDER, + /**Vertical line*/ V, + /**Horizontal line*/ H, + /**Split to left and right children in square style */ SPLIT, + /**Outgoing to right child*/ OUT_R, + /**Outgoing to left child*/ OUT_L, + /**Incoming into left child*/ IN_L, + /**Incoming into right child*/ IN_R, + } + private Function getLabel; private Function getLeft; private Function getRight; + private Function segmentMapper = segment -> switch (segment) { + case D_PLACEHOLDER -> '╳'; // this is unicode diagonal crossing! + case D_LEFT -> '╱'; // this is unicode diagonal! + case D_RIGHT -> '╲'; // this is unicode diagonal! + case S_PLACEHOLDER, V -> '│'; + case H -> '─'; + case SPLIT -> '┴'; + case OUT_R -> '└'; + case OUT_L -> '┘'; + case IN_L -> '┌'; + case IN_R -> '┐'; + }; private PrintStream outStream = System.out; private boolean squareBranches = false; private boolean lrAgnostic = false; - private int hspace = 2; - private int tspace = 1; + private int labelGap = 2; + private int colGap = 1; + private int rowGap = 1; + private boolean usePlaceholder = true; + private boolean flush = true; + /** Create new tree printer with suppliers for the label as well as left and right children. */ public TreePrinter(Function getLabel, Function getLeft, Function getRight) { this.getLabel = getLabel; this.getLeft = getLeft; this.getRight = getRight; } - public void setPrintStream(PrintStream outStream) { - this.outStream = outStream; - } - - public void setSquareBranches(boolean squareBranches) { this.squareBranches = squareBranches; } - - public void setLrAgnostic(boolean lrAgnostic) { this.lrAgnostic = lrAgnostic; } - - public void setHspace(int hspace) { this.hspace = hspace; } - - public void setTspace(int tspace) { this.hspace = tspace; } - - /* - Prints ascii representation of binary tree. - Parameter hspace is minimum number of spaces between adjacent node labels. - Parameter squareBranches, when set to true, results in branches being printed with ASCII box + /** Set stream to which the tree will be printed. Default is {@link System#out}. */ + public TreePrinter setPrintStream(PrintStream outStream) { this.outStream = outStream; return this; } + /** Set square branches style for the tree. It is more compressed than the diagonal view. */ + public TreePrinter setSquareBranches(boolean squareBranches) { this.squareBranches = squareBranches; return this; } + /** Single children will be printed directly below instead of on the left/right. Works only with square branches. */ + public TreePrinter setLrAgnostic(boolean lrAgnostic) { this.lrAgnostic = lrAgnostic; return this; } + /** Set gap between tree labels on the same level. */ + public TreePrinter setLabelGap(int labelSpace) { this.labelGap = labelSpace; return this; } + /** Set gap in single row between trees when printing with {@link #printTrees(List, int)} */ + public TreePrinter setColGap(int tColGap) { this.colGap = tColGap; return this; } + /** Set gap between rows when printing rows with {@link #printTrees(List, int)} */ + public TreePrinter setRowGap(int tRowGap) { this.rowGap = tRowGap; return this; } + /** Configure if placeholder should be used for empty tree labels */ + public TreePrinter setUsePlaceholder(boolean usePlaceholder) { this.usePlaceholder = usePlaceholder; return this; } + /** Configures if output stream will be flushed after printing */ + public TreePrinter setFlush(boolean flush) { this.flush = flush; return this; } + /** Provide a custom set of characters for the tree to be drawn. See {@link Segment}*/ + public TreePrinter setCharacters(Function segmentMapper) { this.segmentMapper = segmentMapper; return this; } + + /** + Prints ascii representation of binary tree.
+ Parameter labelGap is minimum number of spaces between adjacent node labels.
+ Parameter squareBranches, when set to true, results in branches being printed with ASCII box drawing characters. */ public void printTree(T root) { List treeLines = buildTreeLines(root); printTreeLines(treeLines); + if(flush) outStream.flush(); } - /* + /** Prints ascii representations of multiple trees across page. - Parameter hspace is minimum number of spaces between adjacent node labels in a tree. - Parameter tspace is horizontal distance between trees, as well as number of blank lines - between rows of trees. - Parameter lineWidth is maximum width of output - Parameter squareBranches, when set to true, results in branches being printed with ASCII box - drawing characters. + @param lineWidth is maximum width of output + @see #setColGap(int) */ public void printTrees(List trees, int lineWidth) { List> allTreeLines = new ArrayList<>(); @@ -78,14 +115,14 @@ public void printTrees(List trees, int lineWidth) { // first figure range of trees we can print for next row int sumOfWidths = treeWidths[nextTreeIndex]; int endTreeIndex = nextTreeIndex + 1; - while (endTreeIndex < trees.size() && sumOfWidths + tspace + treeWidths[endTreeIndex] < lineWidth) { - sumOfWidths += (tspace + treeWidths[endTreeIndex]); + while (endTreeIndex < trees.size() && sumOfWidths + colGap + treeWidths[endTreeIndex] < lineWidth) { + sumOfWidths += (colGap + treeWidths[endTreeIndex]); endTreeIndex++; } endTreeIndex--; // find max number of lines for tallest tree - int maxLines = allTreeLines.stream().mapToInt(list -> list.size()).max().orElse(0); + int maxLines = allTreeLines.stream().mapToInt(List::size).max().orElse(0); // print trees line by line for (int i = 0; i < maxLines; i++) { @@ -98,12 +135,12 @@ public void printTrees(List trees, int lineWidth) { int rightSpaces = maxRightOffsets[j] - treeLines.get(i).rightOffset; System.out.print(spaces(leftSpaces) + treeLines.get(i).line + spaces(rightSpaces)); } - if (j < endTreeIndex) System.out.print(spaces(tspace)); + if (j < endTreeIndex) System.out.print(spaces(colGap)); } System.out.println(); } - for (int i = 0; i < tspace; i++) { + for (int i = 0; i < rowGap; i++) { System.out.println(); } @@ -143,14 +180,16 @@ private List buildTreeLines(T root) { int spacing = leftTreeLines.get(i).rightOffset - rightTreeLines.get(i).leftOffset; if (spacing > maxRootSpacing) maxRootSpacing = spacing; } - int rootSpacing = maxRootSpacing + hspace; + int rootSpacing = maxRootSpacing + labelGap; if (rootSpacing % 2 == 0) rootSpacing++; // rootSpacing is now the number of spaces between the roots of the two subtrees List allTreeLines = new ArrayList<>(); // strip ANSI escape codes to get length of rendered string. Fixes wrong padding when labels use ANSI escapes for colored nodes. - String renderedRootLabel = rootLabel.replaceAll("\\e\\[[\\d;]*[^\\d;]", ""); + String renderedRootLabel = ANSI_REGEX.matcher(rootLabel).replaceAll(""); + if(renderedRootLabel.isBlank() && usePlaceholder) rootLabel = renderedRootLabel = + squareBranches ? draw(Segment.V) : draw(Segment.D_PLACEHOLDER); // add the root and the two branches leading to the subtrees @@ -165,50 +204,55 @@ private List buildTreeLines(T root) { // there's a right subtree only if (squareBranches) { if (lrAgnostic) { - allTreeLines.add(new TreeLine("\u2502", 0, 0)); + allTreeLines.add(new TreeLine(draw(Segment.V), 0, 0)); } else { - allTreeLines.add(new TreeLine("\u2514\u2510", 0, 1)); + allTreeLines.add(new TreeLine(draw(Segment.OUT_R) + draw(Segment.IN_R),0, 1)); rightTreeAdjust = 1; } } else { - allTreeLines.add(new TreeLine("\\", 1, 1)); + allTreeLines.add(new TreeLine(draw(Segment.D_RIGHT), 1, 1)); rightTreeAdjust = 2; } } - } else if (rightTreeLines.isEmpty()) { - // there's a left subtree only - if (squareBranches) { - if (lrAgnostic) { - allTreeLines.add(new TreeLine("\u2502", 0, 0)); + } else { + if (rightTreeLines.isEmpty()) { + // there's a left subtree only + if (squareBranches) { + if (lrAgnostic) { + allTreeLines.add(new TreeLine(draw(Segment.V), 0, 0)); + } else { + allTreeLines.add(new TreeLine(draw(Segment.IN_L) + draw(Segment.OUT_L), -1, 0)); + leftTreeAdjust = -1; + } } else { - allTreeLines.add(new TreeLine("\u250C\u2518", -1, 0)); - leftTreeAdjust = -1; + allTreeLines.add(new TreeLine(draw(Segment.D_LEFT), -1, -1)); + leftTreeAdjust = -2; } } else { - allTreeLines.add(new TreeLine("/", -1, -1)); - leftTreeAdjust = -2; - } - } else { - // there's a left and right subtree - if (squareBranches) { - int adjust = (rootSpacing / 2) + 1; - String horizontal = String.join("", Collections.nCopies(rootSpacing / 2, "\u2500")); - String branch = "\u250C" + horizontal + "\u2534" + horizontal + "\u2510"; - allTreeLines.add(new TreeLine(branch, -adjust, adjust)); - rightTreeAdjust = adjust; - leftTreeAdjust = -adjust; - } else { - if (rootSpacing == 1) { - allTreeLines.add(new TreeLine("/ \\", -1, 1)); - rightTreeAdjust = 2; - leftTreeAdjust = -2; + // there's a left and right subtree + if (squareBranches) { + int adjust = (rootSpacing / 2) + 1; + String horizontal = String.join("", Collections.nCopies(rootSpacing / 2, + draw(Segment.H))); + String branch = + draw(Segment.IN_L) + horizontal + draw(Segment.SPLIT) + horizontal + draw(Segment.IN_R); + allTreeLines.add(new TreeLine(branch, -adjust, adjust)); + rightTreeAdjust = adjust; + leftTreeAdjust = -adjust; } else { - for (int i = 1; i < rootSpacing; i += 2) { - String branches = "/" + spaces(i) + "\\"; - allTreeLines.add(new TreeLine(branches, -((i + 1) / 2), (i + 1) / 2)); + if (rootSpacing == 1) { + allTreeLines.add(new TreeLine(draw(Segment.D_LEFT) + " " + draw(Segment.D_RIGHT), -1, + 1)); + rightTreeAdjust = 2; + leftTreeAdjust = -2; + } else { + for (int i = 1; i < rootSpacing; i += 2) { + String branches = draw(Segment.D_LEFT) + spaces(i) + draw(Segment.D_RIGHT); + allTreeLines.add(new TreeLine(branches, -((i + 1) / 2), (i + 1) / 2)); + } + rightTreeAdjust = (rootSpacing / 2) + 1; + leftTreeAdjust = -((rootSpacing / 2) + 1); } - rightTreeAdjust = (rootSpacing / 2) + 1; - leftTreeAdjust = -((rootSpacing / 2) + 1); } } } @@ -254,6 +298,10 @@ private static String spaces(int n) { return String.join("", Collections.nCopies(n, " ")); } + private String draw(Segment segment) { + return String.valueOf(segmentMapper.apply(segment)); + } + private static class TreeLine { String line; int leftOffset; diff --git a/src/main/java/CollatzTree.java b/src/test/java/CollatzTree.java similarity index 88% rename from src/main/java/CollatzTree.java rename to src/test/java/CollatzTree.java index 01e8f84..0c362eb 100644 --- a/src/main/java/CollatzTree.java +++ b/src/test/java/CollatzTree.java @@ -1,27 +1,26 @@ +import org.junit.jupiter.api.Test; +import tech.vanyo.treePrinter.TreeNode; import tech.vanyo.treePrinter.TreePrinter; public class CollatzTree { // prints tree diagram for tree representation of "reverse" Collatz sequences - public static void main(String[] args) { + @Test + public void testCollatzSequence() { TreeNode root; - root = collatzTree(15); + root = collatzTree(1, 1, 15); // Collatz Conjecture: for every positive integer X, there is some N such that X appears in collatzTree(N) TreePrinter printer = new TreePrinter<>(n -> ""+n.getValue(), n -> n.getLeft(), n -> n.getRight()); - printer.setHspace(1); + printer.setLabelGap(1); printer.setSquareBranches(true); printer.setLrAgnostic(true); printer.printTree(root); } - private static TreeNode collatzTree(int depth) { - return collatzTree(1, 1, depth); - } - private static TreeNode collatzTree(int start, int curLength, int maxLength) { TreeNode root = new TreeNode(start); if (curLength < maxLength) { diff --git a/src/main/java/CompleteTree.java b/src/test/java/CompleteTree.java similarity index 63% rename from src/main/java/CompleteTree.java rename to src/test/java/CompleteTree.java index afea159..9722a33 100644 --- a/src/main/java/CompleteTree.java +++ b/src/test/java/CompleteTree.java @@ -1,22 +1,23 @@ +import org.junit.jupiter.api.Test; +import tech.vanyo.treePrinter.TreeNode; import tech.vanyo.treePrinter.TreePrinter; public class CompleteTree { - static TreePrinter printer = new TreePrinter<>(n -> ("" + n.getValue()), n -> n.getLeft(), n -> n.getRight()); - - public static void main(String[] args) { - printer.setHspace(2); - printer.setSquareBranches(true); - - TreeNode tree; - tree = completeLevelOrderTree(90); + static TreePrinter printer = new TreePrinter<>(n -> ("" + n.getValue()), TreeNode::getLeft, TreeNode::getRight) + .setLabelGap(2) + .setSquareBranches(true); + @Test + public void levelOrder() { + TreeNode tree = completeLevelOrderTree(90); printer.printTree(tree); - System.out.println(); + } - tree = completeInOrderTree(1, 90); + @Test + public void inOrder() { + TreeNode tree = completeInOrderTree(1, 90); printer.printTree(tree); - System.out.println(); } public static TreeNode completeInOrderTree(int first, int last) { @@ -25,24 +26,24 @@ public static TreeNode completeInOrderTree(int first, int last) { // size = total number of nodes in tree int size = last - first + 1; // number of nodes on next to last level (a power of 2) - int nextToLastLevelCount = maxPowerOf2Under(size/2); + int nextToLastLevelCount = maxPowerOf2Under(size / 2); // number of nodes on last level (which may be less than power of 2) int lastLevelCount = size - (nextToLastLevelCount * 2) + 1; // number of nodes in left subtree int leftSize = nextToLastLevelCount - 1 + Math.min(lastLevelCount, nextToLastLevelCount); int rootVal = first + leftSize; - return new TreeNode(rootVal, completeInOrderTree(first, rootVal-1), completeInOrderTree(rootVal+1, last)); + return new TreeNode(rootVal, completeInOrderTree(first, rootVal - 1), completeInOrderTree(rootVal + 1, last)); } public static TreeNode completeLevelOrderTree(int size) { TreeNode[] nodes = new TreeNode[size]; for (int i = 0; i < size; i++) { - nodes[i] = new TreeNode(i+1); + nodes[i] = new TreeNode(i + 1); } int i = 0; - while (i*2+1 < size) { - nodes[i].setLeft(nodes[i*2+1]); - if (i*2+2 < size) nodes[i].setRight(nodes[i*2+2]); + while (i * 2 + 1 < size) { + nodes[i].setLeft(nodes[i * 2 + 1]); + if (i * 2 + 2 < size) nodes[i].setRight(nodes[i * 2 + 2]); i++; } return nodes[0]; @@ -50,6 +51,6 @@ public static TreeNode completeLevelOrderTree(int size) { public static int maxPowerOf2Under(int limit) { int lzs = Integer.numberOfLeadingZeros(limit); - return 1 << (32-lzs-1); + return 1 << (32 - lzs - 1); } } diff --git a/src/test/java/DrawingTests.java b/src/test/java/DrawingTests.java new file mode 100644 index 0000000..4c6dbbf --- /dev/null +++ b/src/test/java/DrawingTests.java @@ -0,0 +1,61 @@ +import org.junit.jupiter.api.Test; +import tech.vanyo.treePrinter.TreeNode; +import tech.vanyo.treePrinter.TreePrinter; + +import java.util.List; + +public class DrawingTests { + + + TreeNode tree = CompleteTree.completeLevelOrderTree(16); + @Test + public void ansiPrint() { + var ansiPrinter = new TreePrinter<>(n -> ansi8bit(n.getValue()-1), TreeNode::getLeft, TreeNode::getRight); + ansiPrinter.setSquareBranches(true).printTree(tree); + } + + @Test + public void customCharactersPrint() { + var boxyPrinter = new TreePrinter<>(n -> "" + n.getValue(), TreeNode::getLeft, TreeNode::getRight) + .setCharacters(segment -> switch(segment) { + case D_PLACEHOLDER -> '╳'; + case D_LEFT -> '╱'; + case D_RIGHT -> '╲'; + case S_PLACEHOLDER, V -> '║'; + case H -> '═'; + case SPLIT -> '╩'; + case OUT_R -> '╚'; + case OUT_L -> '╝'; + case IN_L -> '╔'; + case IN_R -> '╗'; + }); + boxyPrinter.setSquareBranches(true).printTree(tree); + boxyPrinter.setSquareBranches(false).printTree(tree); + } + + @Test + public void placeholderPrint() { + var boxyPrinter = new TreePrinter<>(n -> (n.getValue() % 2 == 0 ? "" + n.getValue() : ""), + TreeNode::getLeft, + TreeNode::getRight); + boxyPrinter.setSquareBranches(true).printTree(tree); + boxyPrinter.setSquareBranches(false).printTree(tree); + } + + + @Test + public void spacingTestTrees() { + List trees = EnumTrees.enumTrees(4); + + var printer = new TreePrinter<>(n -> "" + n.getValue(), TreeNode::getLeft, TreeNode::getRight); + printer.setColGap(6).setRowGap(2); + printer.setLabelGap(2); + printer.printTrees(trees, 60); + } + + private String ansi8bit(int number) { // maps 0-16 to 30-37, 40-47 + var value = 30 + (number >= 8 ? 10 : 0) + number % 8; + return "\u001b[" + value + "m" + value + "\u001b[0m"; + } + +} diff --git a/src/main/java/EnumDAGTrees.java b/src/test/java/EnumDAGTrees.java similarity index 82% rename from src/main/java/EnumDAGTrees.java rename to src/test/java/EnumDAGTrees.java index 6695290..832241b 100644 --- a/src/main/java/EnumDAGTrees.java +++ b/src/test/java/EnumDAGTrees.java @@ -1,3 +1,5 @@ +import org.junit.jupiter.api.Test; +import tech.vanyo.treePrinter.TreeNode; import tech.vanyo.treePrinter.TreePrinter; import java.util.ArrayList; @@ -11,16 +13,19 @@ public class EnumDAGTrees { // treePrinter doesn't know the difference; it traverses the structure as a tree. // Also note that treePrinter doesn't detect cycles (don't give it graphs with cycles) - public static void main(String[] args) { + @Test + public void enumDAGTrees() { List trees = enumTrees(7); /* We declare a TreePrinter object, parameterized with the type of tree object it will be printing (in this - case TreeNode), and call the TreePrinter constructor, providing lambda functions to get the TreeNode's + case tech.vanyo.treePrinter.TreeNode), and call the TreePrinter constructor, providing lambda functions to get the tech.vanyo.treePrinter.TreeNode's label as a String, and to get the left and right and right subtrees. */ - TreePrinter printer = new TreePrinter<>(n -> ""+n.getValue(), n -> n.getLeft(), n -> n.getRight()); + TreePrinter printer = new TreePrinter<>(n -> ""+n.getValue(), + TreeNode::getLeft, + TreeNode::getRight); // this prints trees in rows across the page printer.setSquareBranches(true); @@ -28,6 +33,7 @@ We declare a TreePrinter object, parameterized with the type of tree object it w } + @SuppressWarnings("unchecked") public static List enumTrees(int n) { List[] subProblems = new ArrayList[n + 1]; diff --git a/src/main/java/EnumTrees.java b/src/test/java/EnumTrees.java similarity index 83% rename from src/main/java/EnumTrees.java rename to src/test/java/EnumTrees.java index 4c00eff..939ad69 100644 --- a/src/main/java/EnumTrees.java +++ b/src/test/java/EnumTrees.java @@ -1,3 +1,5 @@ +import org.junit.jupiter.api.Test; +import tech.vanyo.treePrinter.TreeNode; import tech.vanyo.treePrinter.TreePrinter; import java.util.ArrayList; @@ -8,7 +10,14 @@ public class EnumTrees { // This tests treePrinter by enumerating trees of a given size. // These trees are labelled with either ints or words for ints. - public static void main(String[] args) { + private TreePrinter labelPrinter = new TreePrinter<>( + n -> labelForNode(n.getValue()), + TreeNode::getLeft, + TreeNode::getRight) + .setSquareBranches(true); + + @Test + public void enumTrees() { List trees = enumTrees(6); /* @@ -16,9 +25,7 @@ We declare a TreePrinter object, parameterized with the type of tree object it w case TreeNode), and call the TreePrinter constructor, providing lambda functions to get the TreeNode's label as a String, and to get the left and right and right subtrees. */ - TreePrinter printer = new TreePrinter<>(n -> labelForNode(n.getValue()), n -> n.getLeft(), n -> n.getRight()); - printer.setSquareBranches(true); - printer.printTrees(trees, 120); + labelPrinter.printTrees(trees, 120); } public static List enumTrees(int treeSize) { diff --git a/src/test/java/RandomTree.java b/src/test/java/RandomTree.java new file mode 100644 index 0000000..1587285 --- /dev/null +++ b/src/test/java/RandomTree.java @@ -0,0 +1,90 @@ +import org.junit.jupiter.api.Test; +import tech.vanyo.treePrinter.TreeNode; +import tech.vanyo.treePrinter.TreePrinter; + +import java.util.Random; + +public class RandomTree { + + private static final Random r = new Random(0); // well, that's not a test per-se but a visualization + private static final TreeNode tree = randomTree(30); + + + /* + We declare a TreePrinter object, parameterized with the type of tree object it will be printing (in this + case tech.vanyo.treePrinter.TreeNode), and call the TreePrinter constructor, providing lambda functions to get the tech.vanyo.treePrinter.TreeNode's + label as a String, and to get the left and right and right subtrees. + */ + + private static TreePrinter textualPrinter = + new TreePrinter<>(n -> nameForNumber(n.getValue()), TreeNode::getLeft, TreeNode::getRight); + + private TreePrinter numericPrinter = + new TreePrinter<>(n -> "" + n.getValue(), TreeNode::getLeft, TreeNode::getRight); + + @Test + public void textualPrint() { + textualPrinter + .setLabelGap(1) // set minimum horizontal spacing between node labels with setHspace + .setSquareBranches(true) // use square branches + .printTree(tree); + } + + @Test + public void numericPrint() { + numericPrinter + .setLabelGap(1) + .setSquareBranches(true) + .printTree(tree); + } + + @Test + public void lrAgnosticPrint() { + // single left/right subtree as straight down branch (i.e. no indication of left or right) + numericPrinter + .setLrAgnostic(true) + .setSquareBranches(true) + .printTree(tree); + } + + @Test + public void diagonalPrint() { + numericPrinter.printTree(tree); + } + + @Test + public void hspacePrint() { + numericPrinter + .setLabelGap(3) + .setSquareBranches(true) + .printTree(tree); + } + + public static TreeNode randomTree(int n) { + return randomTree(1, n); + } + + private static TreeNode randomTree(int firstValue, int lastValue) { + if (firstValue > lastValue) return null; + else { + int treeSize = lastValue - firstValue + 1; + int leftCount = r.nextInt(treeSize); + int rightCount = treeSize - leftCount - 1; + TreeNode root = new TreeNode(firstValue + leftCount); + root.setLeft(randomTree(firstValue, firstValue + leftCount - 1)); + root.setRight(randomTree(firstValue + leftCount + 1, lastValue)); + return root; + } + } + + private static String nameForNumber(int n) { + final String[] underTwenty = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", + "ten", "eleven", "twelve", + "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"}; + final String[] decades = {"twenty", "thirty", "fourty", "fifty", "sixty", "seventy", "eighty", "ninety"}; + + if (n < 20) return underTwenty[n]; + else if (n > 99) return "" + n; // not implemented + else return decades[n / 10 - 2] + (n % 10 == 0 ? "" : (" " + underTwenty[n % 10])); + } +}