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.