Are you ready to take your Dart and Flutter skills to the next level? Then join us for the latest installment in the Dart Coding Challenge series! And if you haven't already tackled the previous challenges, now's the time to jump in - they're a fantastic way to build up your expertise and discover some clever ways to accomplish tasks that are unique to Dart and Flutter.
Introducing our newest challenge: "Word Counter"! We're putting your coding skills to the test by asking you to prompt the user for a block of text, and we'll return the number of sentences and words it contains. It may sound easy, but trust us, there are some surprising and creative ways to approach this challenge using Dart and Flutter. So, are you up for the challenge? Let's get coding!
Project Specifications
A command line application that welcomes users to the application, prompts them to enter a body of text that contains at least two words and one sentence.
Calculate the number of individual words and sentences in their body of text and return it to them.
Consider the following characters as the end of a sentence: ! ? .
Treat the following characters as the end of a word: space, comma, colon, or semicolon.
That's it! The rest is up to you. You can take a traditional approach and write the code without relying on some of Dart's built-in functions for an extra challenge. Alternatively, you can make use of Dart's built-in methods to make the task easier.
Give it a shot. And remember, don’t cheat and use a tool like ChatGPT to architect your application for you! You need to think and grow and build synaptic connections! However, after you have written your application, ChatGPT can be a great tool to ask it to look for errors or offer any feedback!
Hints
Use stdin.readLineSync() from dart:io to get input from the command line.
Access the length of a string using the .length getter, such as input.length.
Access individual characters of a string as if it were an List using the index, such as sentence[0] for the first character.
Traverse through a string with a for or foreach loop to perform different actions based on the characters.
Leverage regular expressions with RegExp to simplify the application.
Use the split method with RegExp.
Write unit tests to ensure functionality.
Solution 1
We used the flutter create
command to create our project. This gives us a familiar application structure.
The code and functions for execution of the application are located in the /bin folder, all other functions for the app are stored in the /lib folder, and our unit tests are stored in the /test folder. Let’s look at our files one at a time!
/bin file
import 'package:dart_coding_challenges_3_word_counter/dart_coding_challenges_3_word_counter.dart' as dart_coding_challenges_3_word_counter;
void main(List<String> arguments) {
//Run our app!
dart_coding_challenges_3_word_counter.processInput();
}
This is the most simple file ever! All we do is execute the processInpu
t function from our /lib file.
/lib file
The lib file contains the bulk of the code. Let’s take a look:
import 'dart:io';
//Get our input, verify it has the right # of words, and then display resulsts.
void processInput() {
String text = "";
Map<String, int> counters = {};
do {
text = getInputText();
counters = countWordsAndSentences(text);
} while (counters["words"]! < 2 || counters["sentences"]! < 1);
print("This text has ${counters["sentences"]} sentences and ${counters["words"]} words.");
}
String getInputText() {
String text = "";
do {
//Write a custom prompt based on the required criteria.
if (text.isEmpty) {
stdout.write("Please enter a body of text that is at least one sentence of 2+ words: ");
}
//Trim whitespace from the beginning and end to avoid "phantom" words on counter.
text = stdin.readLineSync()!.trim();
} while (text.isEmpty);
return text;
}
//This is a more classic implementation of solving the problem. We will explore a few other ways to do it, too!
Map<String, int> countWordsAndSentences(String text) {
//The map we will eventually send back,
Map<String, int> results = {"words": 0, "sentences": 0};
//The list of characters that can end a word or a sentence.
//We really only need to use a space to end words, because any , ; or : would be followed with a space anyway!
List<String> endWords = [" "];
List<String> endSentences = ["!", ".", "?"];
//Local counters we can use to se tthe values.
int words = 0;
int sentences = 0;
//A traditional way of solving the problem that does not use all the amazing dart functionality!
int numCharacters = text.length;
//Loop through every characater in the String to see how many word and sentence endings there are.
for (int i = 0; i < numCharacters; i++) {
//Add a word if the current character is found in our end words list.
//If this space is after the end of sentence punctuation we don't want to count it as a word!
if (endWords.contains(text[i]) && !endSentences.contains(text[i - 1])) {
words++;
}
//Add a sentence if the current character is in our end of character list.
//Prevent a sentence like "I am mad!!!" from generating 3 sentences.
if (endSentences.contains(text[i]) && !endSentences.contains(text[i - 1])) {
sentences++;
words++;
}
//If the user did not enter punctuation for the last character we must still classify it as a sentence!
if (i + 1 == numCharacters && !endSentences.contains(text[i])) {
sentences++;
words++;
}
}
results["words"] = words;
results["sentences"] = sentences;
return results;
}
processInput()
//Get our input, verify it has the right # of words, and then display resulsts.
void processInput() {
String text = "";
Map<String, int> counters = {};
do {
text = getInputText();
counters = countWordsAndSentences(text);
} while (counters["words"]! < 2 || counters["sentences"]! < 1);
print("This text has ${counters["sentences"]} sentences and ${counters["words"]} words.");
}
The processInput function runs our main do - while loop, which collects the user data, makes sure that it is valid, and returns the results. It makes use of two other functions: getInputText and countWordsAndSentences. Let’s take a look at them.
getInputText()
String getInputText() {
String text = "";
do {
//Write a custom prompt based on the required criteria.
if (text.isEmpty) {
stdout.write("Please enter a body of text that is at least one sentence of 2+ words: ");
}
//Trim whitespace from the beginning and end to avoid "phantom" words on counter.
text = stdin.readLineSync()!.trim();
} while (text.isEmpty);
return text;
}
This function makes use of a do - while loop to capture input from the user via the command line using the stdin.readLineSync() function. We trim out extraneous whitespace using the trim method, and return the input on the condition that it is not blank.
countWordsAndSentences()
This is the exciting part of the application, and working on a function like this is probably where you spent the majority of your time. Notice at the start we are defining an empty map to store the numbers of words and sentence:
Map<String, int> results = {"words": 0, "sentences": 0};
Then, based on our previous specifications, we are creating two lists, one to store the characters the indicate the end of a word, and a second to store the characters that indicated the end of a sentence.
List<String> endWords = [" "];
List<String> endSentences = ["!", ".", "?"];
Notice that for endWords we are only specifying a space. Why might that be? If you recall in our specifications the following characters can be the end of a word: , : ; or space. What do these all have in common? When they are used (properly) they will always be followed by a space. Consider the following examples:
I love you; I love pizza more.
These are a few of my favorite things: love, pizza, code and pineapples.
Space will be used after comma, semicolon or colon, leaving us only with space to search for. The user may enter invalid text without a space following a comma, but that is beyond our use case for this lesson.
Then we move on and define a few variables.
int words = 0;
int sentences = 0;
//A traditional way of solving the problem that does not use all the amazing dart functionality!
int numCharacters = text.length;
We are creating an int to count the number of words and sentences, and then defining the total number of characters in the variable numCharacters and assigning it using the length method on our input text.
Then we move on to our main loop, which handles the brunt of our calculations.
//Loop through every characater in the String to see how many word and sentence endings there are.
for (int i = 0; i < numCharacters; i++) {
//Add a word if the current character is found in our end words list.
//If this space is after the end of sentence punctuation we don't want to count it as a word!
if (endWords.contains(text[i]) && !endSentences.contains(text[i - 1])) {
words++;
}
//Add a sentence if the current character is in our end of character list.
//Prevent a sentence like "I am mad!!!" from generating 3 sentences.
if (endSentences.contains(text[i]) && !endSentences.contains(text[i - 1])) {
sentences++;
words++;
}
//If the user did not enter punctuation for the last character we must still classify it as a sentence!
if (i + 1 == numCharacters && !endSentences.contains(text[i])) {
sentences++;
words++;
}
}
Note that we create a for loop that assigns an int called i to 0, and then we execute the loop while i is less than the number of the total characters. Each iteration increases the value of i by 1. In short this just means we want to execute one pass of the loop for every character in our body of text.
Then we have several if statements to handle whether we add a word, sentence, or both. We are using our criteria for end of sentences and end of words as specified in our project specifications and stored in the lists, as well as a couple of common edge cases.
The main thing to understand in this statements is that we are using the “contains” method on our list. We are simply asking “is the current character we are on found in our lists?” and if so executing our desired action. Spend a moment going over the code above. Read the comments and make sure you understand what is happening.
Once the loop is done, we update our Map and return it.
results["words"] = words;
results["sentences"] = sentences;
return results;
This gives us all of the functionality we need. Is this the fastest or most “savvy” way of solving this problem? Probably not. It is, however, an easy approach to understand and a great way for beginning programmers to go about it.
/test file
We run several tests on our countWordsAndSentences function to verify output compared with what we would expect.
import 'package:dart_coding_challenges_3_word_counter/dart_coding_challenges_3_word_counter.dart';
import 'package:test/test.dart';
void main() {
test('wordsentencecount', () {
expect(countWordsAndSentences("Hi I love you"), {"words": 4, "sentences": 1});
expect(countWordsAndSentences("Hello, I love you. You are cool! This app is crap"), {"words": 11, "sentences": 3});
expect(countWordsAndSentences("You aren't the type of girl for me, but I still love you."), {"words": 13, "sentences": 1});
expect(countWordsAndSentences("Oh wow, this is really hard to say: I love you."), {"words": 11, "sentences": 1});
expect(countWordsAndSentences("Do you even love me?!?!?! I love you!!!!!"), {"words": 8, "sentences": 2});
expect(countWordsAndSentences("This is not what I meant to say, I think it goes without saying. Will you marry me? Do you love me? I am filled with fear of this fact: that this is all an illusion! Do you feel the same? I do."), {"words": 43, "sentences": 6});
});
}
This is pretty simple. What other tests can you think to add to this test file? Let me know in the comments.
That’s it for our first solution. Time to run our tests:
Time for a spin in the command line:
Solution 2
Solution 2 is a bit of a different beast. For starters, the entirety of this project takes place in our /bin file. This probably isn’t the best practice! Even worse, there are no unit tests written for this project. Your assignment is to refactor this code so it isn’t all stored in the bin file, and create at least one unit test!
Let’s take a look at our /bin file.
import 'dart:io';
void main(List<String> arguments) {
// Define the regular expression for sentence and word splitting
final RegExp sentenceRegExp = RegExp(r'(?<=[.!?])\s+');
final RegExp wordRegExp = RegExp(r'\s+');
// Prompt the user to enter a string
String input;
do {
print('Enter a string:');
input = stdin.readLineSync()!.trim();
} while (input.isEmpty || input.split(wordRegExp).length < 2 || input.split(sentenceRegExp).isEmpty || input.startsWith(' '));
// Count the number of words in the input
List<String> words = input.split(wordRegExp);
int numWords = words.length;
// Count the number of sentences in the input
List<String> sentences = input.split(sentenceRegExp);
int numSentences = sentences.length;
// Display the results
print('The input contains $numWords words and $numSentences sentences.');
}
Wow! This is much shorter. Let’s break down what we did.
First we are creating two variables of type RegExp which contain our criteria for the end of a word and the end of a sentence.
final RegExp sentenceRegExp = RegExp(r'(?<=[.!?])\s+');
This line of code defines a variable called sentenceRegExp
which is a regular expression pattern. A regular expression is a sequence of characters that defines a search pattern, used to match characters in strings. In this case, the regular expression pattern is looking for a specific pattern of characters within a string.
The pattern (?<=[.!?])\s+
is looking for a whitespace character (represented by the \s
), that occurs one or more times (represented by the +
), and is immediately preceded by a period, exclamation point, or question mark (represented by (?<=[.!?])
). So this regular expression is matching one or more whitespace characters that come after the end of a sentence.
The r
before the pattern means that the pattern is a "raw string" - it tells Dart to treat the backslashes and other special characters in the regular expression pattern as literal characters, rather than special escape sequences.
final RegExp wordRegExp = RegExp(r'\s+');
This line of code defines a variable called wordRegExp
which is a regular expression pattern. The pattern r'\s+'
is looking for one or more whitespace characters in a string.
So, whenever you use wordRegExp
to search a string, it will find and match any sequence of one or more whitespace characters, like spaces, tabs, or newlines.
do {
print('Enter a string:');
input = stdin.readLineSync()!.trim();
} while (input.isEmpty || input.split(wordRegExp).length < 2 || input.split(sentenceRegExp).isEmpty || input.startsWith(' '));
Then we move on to our main loop.
This code prompts the user to enter a string and reads the input from the command line. The readLineSync
method reads the input as a string and the !
symbol after it means that the method is expected to return a non-null value. The trim()
method is used to remove any leading or trailing whitespace characters from the string. This is especially important as we are treating spaces as the identifier for a new word.
The code then enters a do-while loop. This loop will continue to execute as long as any one of the following conditions is true:
input
is an empty string (contains no characters).input
contains less than two words. Thesplit
method is used to split theinput
string into a list of words using thewordRegExp
regular expression pattern. If the length of the resulting array is less than two, it means that the input string contained less than two words.input
does not contain any sentences. Thesplit
method is used again, but this time with thesentenceRegExp
regular expression pattern, to split theinput
string into an List of sentences. If this array is empty, it means that the input string did not contain any sentences.input
starts with a space character.
If any of these conditions are true, the prompt to enter a string will be printed again and the user will be prompted to enter a new string until all of the conditions are false.
It’s simple and fairly elegant. Using lists with regular expressions allows the simplification of our code at the expense of a dense and hard to read expression.
// Count the number of words in the input
List<String> words = input.split(wordRegExp);
int numWords = words.length;
// Count the number of sentences in the input
List<String> sentences = input.split(sentenceRegExp);
int numSentences = sentences.length;
// Display the results
print('The input contains $numWords words and $numSentences sentences.');
}
After we have valid input, we count our words, sentences and display them back to the user. You might notice a small inefficiency in this application that I would like you to try and fix.
We are calling input.split twice, once in the do while loop, and then once after it. How might we refactor it to slightly improve our performance speed and remove a few lines of code? I’m not going to solve this one for you — post in the comments how you would solve this problem.
And that’s it! Let’s test our code in the command line.
That’s a wrap for the Dart coding challenge #3. If you haven’t already, be sure to check out challenge 1 and challenge 2, and then head on over to the YouTube channel to pick up some more valuable tips, tricks and hints.