6
\$\begingroup\$
class ChristmasTree
{
    private static void byHand()
    {
        System.out.println( "Chritsmas tree, yey." );
        System.out.println( "     *" );
        System.out.println( "    ***" );
        System.out.println( "   *****" );
        System.out.println( "  *******" );
        System.out.println( " ********" );
        System.out.println( "**********" );
        System.out.println( "    |" );
        System.out.println( "    _" );
    }


    private static void withLoops()
    {
        int rows = 6;
        StringBuilder sb = new StringBuilder();
        for( int row = 0; row < rows; ++row )
        {
            for( int spaces = rows - row; spaces > 0; --spaces )
            {
                sb.append( " " );
            }

            for( int stars = 0; stars < 2 * row + 1; ++stars )
            {
                sb.append( "*" );
            }
            sb.append( "\n" );

        }

        for( int spaces = 0; spaces < rows; ++spaces )
        {
            sb.append( " " );
        }
        sb.append( "|" );
        sb.append( "\n" );
        for( int spaces = 0; spaces < rows; ++spaces )
        {
            sb.append( " " );
        }
        sb.append( "_" );
        System.out.println( sb.toString() );
    }


    private static String spaces( int num )
    {
        StringBuilder sb = new StringBuilder();
        for( int spaces = 0; spaces < num; ++spaces )
        {
            sb.append( " " );
        }
        return sb.toString();
    }

    private static String properly( int rows )
    {
        StringBuilder sb = new StringBuilder();
        for( int row = 0; row < rows; ++row )
        {
            for( int spaces = rows - row; spaces > 0; --spaces )
            {
                sb.append( " " );
            }

            for( int stars = 0; stars < 2 * row + 1; ++stars )
            {
                sb.append( "*" );
            }
            sb.append( "\n" );

        }
        sb.append( spaces( rows ) );
        sb.append( "|" );
        sb.append( "\n" );
        sb.append( spaces( rows ) );
        sb.append( "_" );

        return sb.toString();
    }


    public static void main(String[] args)
    {
        byHand();

        withLoops();

        // What if the tree needs to be 100 lines high?
        // We arrive at the concept of `scalability`.
         System.out.println( properly( 16 ) ); 
    }
}

Topics I have considered but decided against:

  • pass the tree height on the command line via java.util.Scanner;
  • make a copy of spaces() called stars()
  • explain the purpose of the class at the very beginning a la python docstring
  • split the methods so the don't exceed 20 LOC

Also there's this.

\$\endgroup\$
  • \$\begingroup\$ I kept having this nagging feeling your "by hand" tree isn't quite right. You may want to keep all the ending string delimiters in the same column (11 characters after the first) so little typos like this are easier to see. :) \$\endgroup\$ – Peilonrayz Jul 24 at 20:32
4
\$\begingroup\$

As @Doi9t said, most of your code is about creating strings of repeated characters like the method below:

private static String spaces( int num )
{
    StringBuilder sb = new StringBuilder();
    for( int spaces = 0; spaces < num; ++spaces )
    {
        sb.append( " " );
     }
    return sb.toString();
}

You can use instead the Arrays.fill method to create strings of replicated characters like below:

private static String spaces( int num , char c)
{
   char[] arr = new char[num];
   Arrays.fill(arr, c); 

   return new String(arr);
}

About the construction of your tree like this below for nRows = 6 :

Note : \n and whitespaces are invisible

       *      \n <-- row = 0     
      ***     \n   
     *****    \n 
    *******   \n 
   *********  \n
  *********** \n <-- row = 5
       |      \n <-- row = 6   
       _      \n <-- row = 7  

You are doing it using one StringBuilder and consecutive append operations, but you already can calculate the length of the result and where to put characters different from ' ' to minimize operations.
You can check that lines 6, 7, 0 have in common the characteristic to have just one char in the middle, while every line from 1 to 5 can be obtained from the previous one adding one char to the the left and to the right. So if you start from creating a char tree like below:

private static String generateTree(int nRows) {
    final int nColumns = 2 * (nRows + 1);
    final int middle = nColumns / 2;
    char[] tree = new char[(nRows + 2) * nColumns];
    
    //...other instructions

    return new String(tree);
}

After you can calculate every row of your tree putting it in the right position in tree starting from the trunk lines and the peak of your tree:

char[] row = new char[nColumns];  
Arrays.fill(row, ' ');
        
row[middle] = '|';
row[nColumns - 1] = '\n'; //newline will added to every line of the tree.
System.arraycopy(row, 0, tree, nRows  * nColumns, nColumns);
        
row[middle] = '_';
System.arraycopy(row, 0, tree, (nRows + 1) * nColumns, nColumns);
        
row[middle] = '*';
System.arraycopy(row, 0, tree, 0, nColumns);

After you can calculate the other lines of the tree starting from the peak and add them to obtain the final result:

private static String generateTree(int nRows) {
    final int nColumns = 2 * (nRows + 1);
    final int middle = nColumns / 2;
    char[] tree = new char[(nRows + 2) * nColumns];
        
    char[] row = new char[nColumns];  
    Arrays.fill(row, ' ');
        
    row[middle] = '|';
    row[nColumns - 1] = '\n';
    System.arraycopy(row, 0, tree, nRows  * nColumns, nColumns);
        
    row[middle] = '_';
    System.arraycopy(row, 0, tree, (nRows + 1) * nColumns, nColumns);
        
    row[middle] = '*';
    System.arraycopy(row, 0, tree, 0, nColumns);
        
    for (int i = 1; i < nRows; ++i) {
         row[middle - i] = '*';
         row[middle + i] = '*';
         System.arraycopy(row, 0, tree, i * nColumns, nColumns);
    }
        
    return new String(tree);
}
| improve this answer | |
\$\endgroup\$
4
\$\begingroup\$

I have some suggestions for your code.

  1. Most of your loops can be replaced with java.lang.String#repeat (Java 11+)

Example

"*".repeat(10) // Will give you "**********"
  1. I suggest that you create a class constant for each of the tree's parts (*, \n, |) to allow the code to be easier to refactor / make changes in the future, if you need to (i.e., change the star by a plus sign, etc.).

  2. For the tree's parts, I suggest that you use character instead of string, since the character will take less memory than the string (very small amount in this case, but still). " " -> ' '

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ What is your rationale for adding constants in this particular code? In a more complicated setting I would agree, but in this simple example code I think the constants only create unnecessary bloat. \$\endgroup\$ – Roland Illig Jul 24 at 14:07
  • 1
    \$\begingroup\$ For my part, when I have more than one place that uses the same string, I always create a constant / enum for the value. This makes the code easier to refactor, in my opinion, even with small task like this one. \$\endgroup\$ – Doi9t Jul 24 at 15:20
2
\$\begingroup\$

Nice implementation, easy to understand and efficient. Few suggestions:

  1. As others said, by using "*".repeat() you can avoid some of the loops
  2. Make use of String.format it's very powerful for arranging the text
  3. If you want to change the character * is better to use a single constant or even better an enum

Code refactored

It can actually be solved in one line:

String christmasTree = IntStream.range(0, rows+2).mapToObj(i->String.format("%1$"+rows+"s%2$s%n",i<rows?"*".repeat(i+1):i<rows+1?"|":"_",i<rows?"*".repeat(i):"")).collect(Collectors.joining());

Given rows=4 the string christmasTree will be:

   *
  ***
 *****
*******
   |
   _

This is a simplified version with a regular for-loop:

public static String christmasTree(int rows) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < rows; i++) {
        sb.append(String.format("%1$" + rows + "s%2$s%n", "*".repeat(i + 1), "*".repeat(i)));
    }
    sb.append(String.format("%1$" + rows + "s%n", "|"));
    sb.append(String.format("%1$" + rows + "s", "_"));
    return sb.toString();
}

Explanation

The key methods are .repeat() (as @Doi9t suggested) and String.format.

Regarding the line in the for-loop, the parameters of String.format() are described below:

  • %1$ means take the first argument, which is "*".repeat(i + 1)
  • %2$ means take the second argument, which is "*".repeat(i)
  • s means convert the argument to a String
  • %1$" + rows + "s pads the first string argument to rows characters
  • %n represents the line terminator

More about String.format (which uses a java.util.Formatter).

This is a version with a configurable ornament:

public static String christmasTree(int rows, Ornament ornament) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < rows; i++) {
        sb.append(String.format("%1$" + rows + "s%2$s%n", ornament.getValue().repeat(i + 1), ornament.getValue().repeat(i)));
    }
    sb.append(String.format("%1$" + rows + "s%n", "|"));
    sb.append(String.format("%1$" + rows + "s", "_"));
    return sb.toString();
}

The Ornament enum:

public enum Ornament{
    BULBS("o"),
    STARS("*");
    
    private String value;
    
    Ornament(String value){
        this.value=value;
    }
    
    public String getValue() {
        return this.value;
    }
}

Merry Christmas in advance System.out.println(christmasTree(10,Ornament.BULBS))

         o
        ooo
       ooooo
      ooooooo
     ooooooooo
    ooooooooooo
   ooooooooooooo
  ooooooooooooooo
 ooooooooooooooooo
ooooooooooooooooooo
         |
         _
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.