Solution for Programming Exercise 6.8
This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.
Exercise 6.8:
Write a program that has a TextArea where the user can enter some text. The program should have a button such that when the user clicks on the button, the program will count the number of lines in the user's input, the number of words in the user's input, and the number of characters in the user's input. This information should be displayed on three Labels. Recall that if textInput is a TextArea, then you can get the contents of the TextArea by calling the function textInput.getText(). This function returns a String containing all the text from the text area. The number of characters is just the length of this String. Lines in the String are separated by the new line character, '\n', so the number of lines is just the number of new line characters in the String, plus one. Words are a little harder to count. Exercise 3.4 has some advice about finding the words in a String. Essentially, you want to count the number of characters that are first characters in words. Here is a picture of my solution:
The window contains five components. There are several ways to lay them out. Since the TextArea is much larger than the other components, my program uses a VBox as the root of the scene graph, which allows each component to have its preferred height.
To get a dark blue border around the edges, I use CSS to add a border to the VBox. To get blue lines between the components, I set the spacing between components in the VBox to be 4, and I make its background dark blue. I set the background color for the labels to be white; otherwise, the usual transparent background for the labels would let the blue background of the VBox show through. I increased the maximum width of the labels to make them fill the entire width of the VBox. (Otherwise, the blue background would show through in the space left empty by the labels.) As for the button, I did not want to increase its maximum width to fill the available space, but without that, the VBox placed it on the left edge of the available space. I wanted it to be centered. The only way I could think of to do that was to put the button inside another pane, and add that pane to the VBox. That pane will fill the available space. I used a BorderPane with the button as its center component; the parameter to the BorderPaneconstructor becomes the center component in the layout, and a BorderPane centers its center component within the available space. You can see how it's is all done in this code from the start() method:
textInput = new TextArea(); textInput.setPrefRowCount(15); textInput.setPrefColumnCount(30); /* Create the button and a listener to listen for clicks on the button. */ Button countButton = new Button("Process the Text"); countButton.setOnAction( e -> processInput() ); /* Create each of the labels, and set their properties. */ String style = "-fx-padding: 5px; -fx-font: bold 14pt serif; -fx-background-color: white"; lineCountLabel = new Label(" Number of lines:"); lineCountLabel.setStyle(style); lineCountLabel.setMaxWidth(1000); wordCountLabel = new Label(" Number of words:"); wordCountLabel.setStyle(style); wordCountLabel.setMaxWidth(1000); charCountLabel = new Label(" Number of chars:"); charCountLabel.setStyle(style); charCountLabel.setMaxWidth(1000); /* Use a VBox as the root component. */ VBox root = new VBox( 4, textInput, new BorderPane(countButton), lineCountLabel, wordCountLabel, charCountLabel ); root.setStyle( "-fx-background-color: #009; -fx-border-color: #009; -fx-border-width:3px" );
The first parameter to the VBox constructor is the spacing between components, and the remaining parameter are added as child nodes of the VBox. Note that both countButton IS wrapped in a pane which is then added to the VBox.
The ActionEvent handler for the button calls the method processInput(). That method does all the work of the program: It gets the text from the TextArea, does the counting, and sets the labels. The only interesting part is counting the words. Back in Exercise 3.4, words such as "can't", that contain an apostrophe, were counted as two words. This time around, let's handle this special case. Two letters with an apostrophe between them should be counted as part of the same word. The algorithm for counting words is still
wordCt = 0 for each character in the string: if the character is the first character of a word: Add 1 to wordCt
but testing whether a given character is the first character in a word has gotten more complicated. To make the test easier, I use a boolean variable, startOfWord. The value of this variable is set to true if the character is the start of a word and to false if not. That is, the algorithm becomes:
wordCt = 0 for each character in the string: Let startOfWord be true if at start of word, false otherwise if startOfWord is true: Add 1 to wordCt
The use of a "flag variable" like startOfWord can simplify the calculation of a complicated boolean condition. The value is computed as a series of tests:
boolean startOfWord; // Is character i the start of a word? if ( Character.isLetter(text.charAt(i)) == false ) startOfWord = false; // No. It's not a letter. else if (i == 0) startOfWord = true; // Yes. It's a letter at start of text. else if ( Character.isLetter(text.charAt(i-1)) ) startOfWord = false; // No. It's a letter preceded by a letter. else if ( text.charAt(i-1) == '\'' && i > 1 && Character.isLetter(text.charAt(i-2)) ) startOfWord = false; // No. It's a continuation of a word // after an apostrophe. else startOfWord = true; // Yes. It's a letter preceded by // a non-letter.
The first test checks whether the character in position i is a letter. If it is not, then we know that it can't be the start of a word, so startOfWord is false. If it is a letter, it might be the start of a word, so we go on to make additional tests. Note that if we get to the other tests at all, we already know that the character in position i is a letter. And so on. This style of "cascading tests" is very useful. In each test, we already have all the information from the previous tests. Note that the cascade effect works only with "else if". Using "if" in place of "else if" in the preceding code would not give the right answer. (You should be sure to understand why this is so.) You should also note why the test "if (i == 0)"" has to be made before the test if ( Character.isLetter(text.charAt(i-1)) )"—that's because text.charAt(i-1) gives an index-out-of-bounds exception if i is zero.
import javafx.application.Application; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextArea; /** * In this program, the user types some text in a TextArea and presses * a button. The program computes and displays the number of lines * in the text, the number of words in the text, and the number of * characters in the text. A word is defined to be a sequence of * letters, except that an apostrophe with a letter on each side * of it is considered to be a letter. (Thus "can't" is one word, * not two.) */ public class TextCounter extends Application { public static void main(String[] args) { launch(args); } //--------------------------------------------------------------------- private TextArea textInput; // For the user's input text. private Label lineCountLabel; // For displaying the number of lines. private Label wordCountLabel; // For displaying the number of words. private Label charCountLabel; // For displaying the number of chars. /** * The constructor creates components and lays out the window. */ public void start(Stage stage) { textInput = new TextArea(); textInput.setPrefRowCount(15); textInput.setPrefColumnCount(30); /* Create the button and a listener to listen for clicks on the button. */ Button countButton = new Button("Process the Text"); countButton.setOnAction( e -> processInput() ); /* Create each of the labels, and set their properties. */ String style = "-fx-padding: 5px; -fx-font: bold 14pt serif; -fx-background-color: white"; lineCountLabel = new Label(" Number of lines:"); lineCountLabel.setStyle(style); lineCountLabel.setMaxWidth(1000); wordCountLabel = new Label(" Number of words:"); wordCountLabel.setStyle(style); wordCountLabel.setMaxWidth(1000); charCountLabel = new Label(" Number of chars:"); charCountLabel.setStyle(style); charCountLabel.setMaxWidth(1000); /* Use a VBox as the root component. */ VBox root = new VBox( 4, textInput, new BorderPane(countButton), lineCountLabel, wordCountLabel, charCountLabel ); root.setStyle("-fx-background-color: #009; -fx-border-color: #009; -fx-border-width:3px"); Scene scene = new Scene(root); stage.setScene(scene); stage.setTitle("Line/Word/Char Counter"); stage.setResizable(false); stage.show(); } // end constructor /** * This will be called when the user clicks the "Process the Text" button. * It gets the text from the text area, counts the number of chars, words, * and lines that it contains, and sets the labels to display the results. */ public void processInput() { String text; // The user's input from the text area. int charCt, wordCt, lineCt; // Char, word, and line counts. text = textInput.getText(); charCt = text.length(); // The number of characters in the // text is just its length. /* Compute the wordCt by counting the number of characters in the text that lie at the beginning of a word. The beginning of a word is a letter such that the preceding character is not a letter. This is complicated by two things: If the letter is the first character in the text, then it is the beginning of a word. If the letter is preceded by an apostrophe, and the apostrophe is preceded by a letter, than its not the first character in a word. */ wordCt = 0; for (int i = 0; i < charCt; i++) { boolean startOfWord; // Is character i the start of a word? if ( Character.isLetter(text.charAt(i)) == false ) startOfWord = false; // No. It's not a letter. else if (i == 0) startOfWord = true; // Yes. It's a letter at start of text. else if ( Character.isLetter(text.charAt(i-1)) ) startOfWord = false; // No. It's a letter preceded by a letter. else if ( text.charAt(i-1) == '\'' && i > 1 && Character.isLetter(text.charAt(i-2)) ) startOfWord = false; // No. It's a continuation of a word // after an apostrophe. else startOfWord = true; // Yes. It's a letter preceded by // a non-letter. if (startOfWord) wordCt++; } /* The number of lines is just one plus the number of times the end of line character, '\n', occurs in the text. */ lineCt = 1; for (int i = 0; i < charCt; i++) { if (text.charAt(i) == '\n') lineCt++; } /* Set the labels to display the data. */ lineCountLabel.setText(" Number of lines: " + lineCt); wordCountLabel.setText(" Number of words: " + wordCt); charCountLabel.setText(" Number of chars: " + charCt); } // end processInput() } // end class TextCounter