12
\$\begingroup\$

I just started learning Java about a month ago. I'm wondering, what would an experienced developer do differently in the code below? As I learn to code, what direction should I be pushing myself?

I wanted this program to output large versions of digits that the user enters. Any characters entered that are not digits should just be rendered as a space. If the user enters more than 8 characters, I truncate it to avoid running off the edge of my terminal. The program compiles and works right.

Sorry it's kind of long, but it's not very complex.

import java.util.*;

/**
 * This program displays digits in big characters.
 */
public class BigDigits {
    static final int HEIGHT = 3;
    static final int WIDTH = 7;
    // CHARMAP contains the encoding for each digit
    static final int[][] CHARMAP = {
        {1,1,1,1,1,1,0},
        {1,1,0,0,0,0,0},
        {1,0,1,1,0,1,1},
        {1,1,1,0,0,1,1},
        {1,1,0,0,1,0,1},
        {0,1,1,0,1,1,1},
        {0,1,1,1,1,1,1},
        {1,1,0,0,0,1,0},
        {1,1,1,1,1,1,1},
        {1,1,1,0,1,1,1},
    };

    public static void main(String [] args) {

        char again = 'y';

        printIntro();
        while (again == 'y' || again == 'Y') {
            String s = getString();
            printString(s);
            again = goAgain();
        }
    }

    /**
     * Prints a short introduction to the program
     */
    static void printIntro() {
        System.out.println("This program displays a number in large digits.");
    }

    /**
     * Retrieves the digits from the user as a String
     */
    static String getString() {

        String response;

        Scanner in = new Scanner(System.in);
        System.out.print("Please enter a number: ");
        response = in.nextLine();
        System.out.println();
        response = truncateString(response);
        return response;
    }

    /**
     * Truncates the String to 8 digits if necessary
     */
    static String truncateString(String s) {

        if (s.length() > 8) {
            s = s.substring(0,8);
        }
        return s;
    }

    /**
     * Prints the number in large digits
     */
    static void printString(String s) {

        String a = getLine(s, "top");
        String b = getLegs(s, "top");
        String c = getLine(s, "mid");
        String d = getLegs(s, "bot");
        String e = getLine(s, "bot");

        System.out.println(a);
        for (int i = 0; i < HEIGHT; i++) {
            System.out.println(b);
            }
        System.out.println(c);
        for (int i = 0; i < HEIGHT; i++) {
            System.out.println(d);
            }
        System.out.println(e);
        System.out.println();
    }

    /**
     * Builds the top, middle and bottom horizontal lines
     */
    static String getLine(String s, String w) {

        int n; // Where to look in a digit's CHARMAP array (top, mid, or bot)
        char digitI; // The i-th digit from the input String
        char c; // The character to be printed to output (- or space)

        String r = " "; // Begin the line

        // Pick which entry in CHARMAP for each digit
        if (w.equals("top")) {
            n = 5;
        } else if (w.equals("bot")) {
            n = 2;
        } else {
            n = 6;
        }

        // Build the line
        for (int i = 0; i < s.length(); i++) {

            digitI = s.charAt(i);

            if (digitI >= '0' && 
                    digitI <= '9' && 
                    CHARMAP[Character.getNumericValue(digitI)][n] == 1) {
                c = '-';
            } else {
                c = ' ';
            }

            for (int j = 0; j < WIDTH; j++) {
                r = r + c;
            }
            r = r + "      ";
        }

        return r; // The finished line
    }

   /**
    * Builds the upper and lower legs
    */
   static String getLegs(String s, String w) {

        int n1; // Where to look in a digit's charmap array for the two legs
        int n2;
        char digitI; // The i-th digit in the input String
        char c1; // The character to be printed to output ('|' or space)
        char c2;

        String r = ""; // Begin the line

        // Pick the digit's entry in the CHARMAP array
        if (w.equals("top")) {
            n1 = 4;
            n2 = 0;
        } else {
            n1 = 3;
            n2 = 1;
        }

        // Build the line
        for (int i = 0; i < s.length(); i++) {

            digitI = s.charAt(i);

            // The left leg
            if (digitI >= '0' &&
                    digitI <= '9' &&
                    CHARMAP[Character.getNumericValue(digitI)][n1] == 1) {
                c1 = '|';
            } else {
                c1 = ' ';
            }

            // The right leg
            if (digitI >= '0' &&
                    digitI <= '9' &&
                    CHARMAP[Character.getNumericValue(digitI)][n2] == 1) {
                c2 = '|';
            } else {
                c2 = ' ';
            }

            r = r + c1;
            for (int j = 0; j < WIDTH; j++) {
                r = r + " ";
            }
            r = r + c2;
            r = r + "    ";
        }

        return r; // The finished line
    }

    /**
    * Asks the user whether to run program again
    */
    static char goAgain() {   

        char response;

        Scanner in = new Scanner(System.in);
        System.out.print("--> Again? [y/n] ");
        response = in.next().charAt(0);
        System.out.println();
        return response;
    }
}

Example run:

This program displays a number in large digits.
Please enter a number: 12345

              -------      -------                   -------      
        |            |            |    |       |    |            
        |            |            |    |       |    |            
        |            |            |    |       |    |            
              -------      -------      -------      -------      
        |    |                    |            |            |    
        |    |                    |            |            |    
        |    |                    |            |            |    
              -------      -------                   -------      

--> Again? [y/n] y

Please enter a number: 67890

 -------      -------      -------      -------      -------      
|                    |    |       |    |       |    |       |    
|                    |    |       |    |       |    |       |    
|                    |    |       |    |       |    |       |    
 -------                   -------      -------                   
|       |            |    |       |            |    |       |    
|       |            |    |       |            |    |       |    
|       |            |    |       |            |    |       |    
 -------                   -------      -------      -------      

--> Again? [y/n] n
\$\endgroup\$
11
\$\begingroup\$

When presented with problems like this it is common to decode/encode the input characters once, and store the decoded output in a structure that helps you on the presentation layer.

First, though your control loops and input management...

User Interaction

I recommend you create a single class to encapsulate the Scanner instance that should only be created once, but which you create each time you need to get user input. A ui instance that allows you to query the user actions would be so much better. Consider:

import java.io.InputStream;
import java.util.Scanner;

public class UserInput implements AutoCloseable {

    private final Scanner scanner;
    private final boolean closeSource;

    public UserInput(InputStream source, boolean closeSource) {
        this.scanner = new Scanner(source);
        this.closeSource = closeSource;
    }

    @Override
    public void close() {
        if (closeSource) {
            scanner.close();
        }
    }

    private String prompt(String message) {
        try {
            System.out.print(message + ": ");
            return scanner.nextLine();
        } finally {
            System.out.println();
        }
    }

    public String getLine(int maxlength) {
        String line = prompt("Please enter a number").trim();
        return line.length() <= maxlength ? line : line.substring(0, maxlength);
    }

    public boolean doAgain() {
        return prompt("--> Do Again? [y/n]").toLowerCase().startsWith("y");
    }

}

That's a bit complciated... it has an auto-closable feature which may, or may not close the scanner when it is dismissed. The point, though, is that it lasts for the length of the program, and it is reusable. It has methods which prompt for user input, and interprets the response. The doAgain() method is important because it is what allows the main program loop to be super-simple:

do {
    // stuff
} while (ui.doAgain());

The other prompt allows you to simplify the other aspect of user input....

This is a summary of what my main method looks like, with the ui instance available:

public static void main(String[] args) {
    try (UserInput ui = new UserInput(System.in, false)) {
        System.out.println("This program displays a number in large digits.");
        do {
            String toprint = ui.getLine(8);
            String output = ..... *create string that represents the line to print.
            System.out.println(output);
        } while (ui.doAgain());
    }
}

Note how much more logical that main method is.

Digits

In this case, because you have to transform from different digits following each other on the line, and because each digit spans different lines, I would probably create a "Digit" class which has a method "getRow(x)" where the rows are from top-to-bottom..... For example, the 0 Digit will output lines like:

line 0:      ------- 
line 1:     |       |
......
line 7:      ------- 

Since there are only 5 types of output:

1:  -------    <- full row
2: |       |   <- both sides
3: |           <- left side
4:         |   <- right side
5:             <- nothing

and, also since rows 1, 2, and 3 are always the same, and since rows 5, 6, and 7 are always the same, it's relatively easy to break the digits in to 5 sections..... which I will call the: head, thorax, waist, legs, and feet. Each section can have just one output.

Then, the right construct to hold that all, is a set of 2 enums... one for rows of text, and the other for the complete digits. The lines enum does not need to be public. The process is somewhat verbose, but, the mapping has to be manual, so there's no way around it.

Turning the above in to a discrete class/file, I get the following, with a main method for testing:

import java.util.stream.Collectors;
import java.util.stream.IntStream;

enum Line {
    FULL (" ------- "),
    EMPTY("         "),
    LEFT ("|        "),
    RIGHT("        |"),
    BOTH ("|       |");

    private final String text;
    Line(String txt) {
        this.text = txt;
    }

    public String toString() {
        return text;
    }

}

public enum Digit {
    ZERO (Line.FULL,  Line.BOTH,  Line.EMPTY, Line.BOTH,  Line.FULL ),
    ONE  (Line.EMPTY, Line.RIGHT, Line.EMPTY, Line.RIGHT, Line.EMPTY),
    TWO  (Line.FULL,  Line.RIGHT, Line.FULL,  Line.LEFT,  Line.FULL ),
    THREE(Line.FULL,  Line.RIGHT, Line.FULL,  Line.RIGHT, Line.FULL ),
    FOUR (Line.EMPTY, Line.BOTH,  Line.FULL,  Line.RIGHT, Line.EMPTY),
    FIVE (Line.FULL,  Line.LEFT,  Line.FULL,  Line.RIGHT, Line.FULL ),
    SIX  (Line.EMPTY, Line.LEFT,  Line.FULL,  Line.BOTH,  Line.FULL ),
    SEVEN(Line.FULL,  Line.RIGHT, Line.EMPTY, Line.RIGHT, Line.EMPTY),
    EIGHT(Line.FULL,  Line.BOTH,  Line.FULL,  Line.BOTH,  Line.FULL ),
    NINE (Line.FULL,  Line.BOTH,  Line.FULL,  Line.RIGHT, Line.EMPTY);

    private final Line head, thorax, waist, legs, feet;

    private Digit(Line head, Line thorax, Line waist, Line legs, Line feet) {
        this.head = head;
        this.thorax = thorax;
        this.waist = waist;
        this.legs = legs;
        this.feet = feet;
    }

    public String getRow(int row) {
        switch (row) {
        case 0:
            return head.toString();
        case 1: 
        case 2: 
        case 3: 
            return thorax.toString();
        case 4: 
            return waist.toString();
        case 5: 
        case 6: 
        case 7: 
            return legs.toString();
        case 8: 
            return feet.toString();
        default:
            throw new IllegalStateException("No such row " + row);
        }
    }

    public String toString() {
        return IntStream.range(0, 9)
                .mapToObj(this::getRow)
                .collect(Collectors.joining("\n"));
    }

    public static void main(String[] args) {
        for (Digit d : Digit.values()) {
            System.out.println("Digit : " + d.ordinal());
            System.out.println(d);
        }
    }

}

Sure, it's long winded, but, look what happens when you want to use it in your code. Here is some code that transforms an array of Digits in to an output String:

private static final String buildBig(Digit[] digits) {
    return IntStream.range(0, 9)
            .mapToObj(line -> getLine(line, digits))
            .collect(Collectors.joining("\n"));
}

private static final String getLine(int line, Digit...digits) {
    return Arrays.stream(digits)
            .map(d -> d.getRow(line))
            .collect(Collectors.joining("    "));
}

The first method breaks the digits in to their 9 rows/lines. The second method runs through each digit and builds a single line for all the digits. The digits are separated by some spaces, and the lines are separated by newlines.

Input to Digits

The missing piece now, is how do you convert the input line to the available digits. Here's one way:

private static final int MAX_DIGIT = Digit.values().length - 1;

private static final Digit[] scan(String makebig) {
    return IntStream.range(0, makebig.length())
            .map(c -> makebig.charAt(c))
            .filter(Character::isDigit)
            .map(Character::getNumericValue)
            .filter(d -> d <= MAX_DIGIT)
            .mapToObj(d -> Digit.values()[d])
            .toArray(s -> new Digit[s]);
}

That code goes through each character, checks to see if it is a digit, then converts it to a Digit enum value.

Conclusion

What's important, is that the code separates out the digits, the user interaction, and the controlling code. This makes the main class a bunch simpler. Here's the controlling class I have:

import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class BigDigits {

    public static void main(String[] args) {
        try (UserInput ui = new UserInput(System.in, false)) {
            System.out.println("This program displays a number in large digits.");
            do {
                String toprint = ui.getLine(8);
                Digit[] digits = scan(toprint);
                String output = buildBig(digits);
                System.out.println(output);
            } while (ui.doAgain());
        }
    }

    private static final int MAX_DIGIT = Digit.values().length - 1;

    private static final Digit[] scan(String makebig) {
        return IntStream.range(0, makebig.length())
                .map(makebig::charAt)
                .filter(Character::isDigit)
                .map(Character::getNumericValue)
                .filter(d -> d <= MAX_DIGIT)
                .mapToObj(d -> Digit.values()[d])
                .toArray(s -> new Digit[s]);
    }

    private static final String buildBig(Digit[] digits) {
        return IntStream.range(0, 9)
                .mapToObj(line -> getLine(line, digits))
                .collect(Collectors.joining("\n"));
    }

    private static final String getLine(int line, Digit...digits) {
        return Arrays.stream(digits)
                .map(d -> d.getRow(line))
                .collect(Collectors.joining("    "));
    }

}

All in all, the logic is simpler, and in the right places.

On the other hand, I want to compliment you on your variable names, the style which is consistent, and neat.

Additionally, you have it working well, I could not reasonably break it, or find other bugs. I would say it is a good solution, but some refactoring, and abstraction, would make it better.

Thank you for the interesting question, and the challenge it gave me to play with.

\$\endgroup\$
  • 1
    \$\begingroup\$ I love the way you named the parameters head, thorax, waist, legs, and feet. I will never look at digital displays the seven-segment displays the same way again. \$\endgroup\$ – 200_success Oct 18 '15 at 5:08
  • \$\begingroup\$ It might be a good idea to handle the possible NoSuchElementException from prompt(). \$\endgroup\$ – 200_success Oct 18 '15 at 5:10

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.