Chapters

Hide chapters

Dart Apprentice: Beyond the Basics

First Edition · Flutter · Dart 2.18 · VS Code 1.71

Dart Apprentice: Beyond the Basics

Section 1: 15 chapters
Show chapters Hide chapters

1. String Manipulation
Written by Jonathan Sande

If you came to this chapter hoping to learn how to knit or crochet, you’ll have to find another book. In this chapter, you’ll learn how to manipulate text by adding and removing characters, splitting and re-joining strings, and performing search-and-replace operations. Another essential skill this chapter will teach you is how to validate user input. Regular expressions are a powerful tool for that, and in addition to string validation, you can also use them to extract text. Hold on to your hat because you’re about to say goodbye to the land of beginners.

Basic String Manipulation

This section will start with a few easy ways to modify strings. These string manipulation methods are easy because they’re built right into the String type. Anytime you have a string, they’re just one . away.

Changing the Case

Strings are case sensitive, which means Hello is different than hello, which is different than HELLO. This can be a problem if you’re using email addresses as unique identifiers in your database. Email addresses are inherently not case sensitive. You don’t want to create different user accounts for spongebob@example.com and SpongeBob@example.com, do you? And then there are always those users who are still living off memes from the last decade and give you sPoNgEbOb@eXaMpLe.cOm. No worries, though. Dart is here to save the day.

Write the following code in main:

const userInput = 'sPoNgEbOb@eXaMpLe.cOm';
final lowercase = userInput.toLowerCase();
print(lowercase);

The method toLowerCase creates a new string where all the capital letters are lowercase.

Run that, and you’ll get a string your database will thank you for:

spongebob@example.com

If you wish to go the other way, you can call toUpperCase.

Adding and Removing at the Ends

The beginning or end of a string sometimes needs a little work to create the form you want.

Trimming

One common thing you’ll want to remove is extra whitespace at the beginning or end of a string. Whitespace can be problematic because two strings might appear to be the same but are actually different. Removing this whitespace is called trimming.

Replace the contents of main with the following:

const userInput = ' 221B Baker St.   ';
final trimmed = userInput.trim();

print(trimmed); // '221B Baker St.'

trimmed no longer contains the extra spaces at the beginning or end of the string. This works for not only the space character but also the newline character, tab character or any other Unicode-defined White_Space character.

Use trimLeft or trimRight if you only need to trim whitespace from one end.

Padding

In contrast to trimming, sometimes you need to add extra space or other characters to the beginning or end of a string. For example, what if you’re making a digital clock? The naive approach would be to form your string like so:

final time = Duration(hours: 1, minutes: 32, seconds: 57);
final hours = time.inHours;
final minutes = time.inMinutes % 60;
final seconds = time.inSeconds % 60;
final timeString = '$hours:$minutes:$seconds';
print(timeString); // 1:32:57

You need to take the remainder after dividing by 60 to get minutes and seconds because there might be more than 59 minutes and seconds in some duration, which is true in this case where the total duration is over an hour.

Running the code above gives a result of 1:32:57. This is reasonable for a digital clock. However, changing the duration slightly will show the problem. Replace the first line above with the following:

final time = Duration(hours: 1, minutes: 2, seconds: 3);

Rerun your code, and you’ll see the new result of timeString:

1:2:3

That doesn’t look much like a time string anymore. What you want is 1:02:03.

Dart is here to the rescue again, this time with the padLeft method. You can use padLeft to add any character, but in this case, you want to add zeros to the left of numbers less than 10.

Replace the code above with the new version:

final time = Duration(hours: 1, minutes: 2, seconds: 3);
final hours = time.inHours;
final minutes = '${time.inMinutes % 60}'.padLeft(2, '0');
final seconds = '${time.inSeconds % 60}'.padLeft(2, '0');
final timeString = '$hours:$minutes:$seconds';
print(timeString);

The 2 in padLeft(2, '0') means you want the minimum length to be two characters long. The '0' is the padding character you want to use. If you hadn’t specified that, the padding would have defaulted to the space character.

Run the code again. This time, you’ll see the following result:

1:02:03

That’s much better.

As you might have guessed, you can also use a padRight method to add characters to the end of a string.

Splitting and Joining

Developers often use strings to combine many small pieces of data. One such example is the lines of a comma-separated values (CSV) file. In such a file, each line contains data items called fields, which commas separate. Here’s a sample file:

Martin,Emma,12,Paris,France
Smith,John,37,Chicago,USA
Weber,Hans,52,Berlin,Germany
Bio,Marie,33,Cotonou,Benin
Wang,Ming,40,Shanghai,China
Hernández,Isabella,23,Mexico City,Mexico
Nergui,Bavuudorj,21,Ulaanbaatar,Mongolia

The fields in this CSV file are ordered by surname, given name, age, city and country.

Take just one line of that file. Here’s how you would split that string at the commas to access the fields:

const csvFileLine = 'Martin,Emma,12,Paris,France';
final fields = csvFileLine.split(',');
print(fields);

The split method can split the string by any character, but here you specify that you want it to split at ','.

Run that code, and you’ll see that fields contains a list of strings like so:

[Martin, Emma, 12, Paris, France]

Note that those are all separate strings now, which you can easily access. You learned how to access the elements of a list in Dart Apprentice: Fundamentals, Chapter 12, “Lists”.

You can also go the other direction. Given some list of strings, you can join all the elements together using the join method on List. This time use a dash instead of a comma for a little extra variety:

final joined = fields.join('-');

Print joined, and you’ll see the following result:

Martin-Emma-12-Paris-France

Replacing

Find-and-replace is a common task you perform on any text document. You can also do the same thing programmatically. For example, say you want to replace all the spaces with underscores in some text. You can do this easily using the replaceAll method.

Write the following in main:

const phrase = 'live and learn';
final withUnderscores = phrase.replaceAll(' ', '_');
print(withUnderscores);

The first argument you give to replaceAll is the string you want to match — in this case, the space character. The second argument is the replacement string, in this case, an underscore.

Run the code above, and you’ll see the following result:

live_and_learn

If you only need to replace the first occurrence of some pattern, use replaceFirst instead of replaceAll.

Exercises

  1. Take a multiline string, such as the text below, and split it into a list of single lines. Hint: Split at the newline character.
France
USA
Germany
Benin
China
Mexico
Mongolia
  1. Find an emoji online to replace :] with in the following text:
How's the Dart book going? :]

Building Strings

You learned about string concatenation in Dart Apprentice: Fundamentals, Chapter 4, “Strings”, with the following example:

var message = 'Hello' + ' my name is ';
const name = 'Ray';
message += name;
// 'Hello my name is Ray'

But using the + operator isn’t efficient when building up long strings one piece at a time. The reason is that Dart strings are immutable — that is, they can’t be changed — so every time you add two strings together, Dart has to create a new object for the concatenated string.

Improving Efficiency With String Buffers

A more efficient method of building strings is to use the StringBuffer class. The word “buffer” refers to a storage area you can modify in the computer’s memory. StringBuffer allows you to add strings to the internal buffer without needing to create a new object every time. When you finish building the string, you just convert the StringBuffer contents to String.

Here’s the previous example rewritten using a string buffer:

final message = StringBuffer();
message.write('Hello');
message.write(' my name is ');
message.write('Ray');
message.toString();
// 'Hello my name is Ray'

Calling toString converts the string buffer to the String type. This is like the type conversion you’ve seen when calling toInt to convert a double to the int type.

Building Strings in a Loop

Typically, you’ll use a string buffer inside a loop, where every iteration adds a little more to the string.

Write the following for loop in main:

for (int i = 2; i <= 1024; i *= 2) {
  print(i);
}

This prints powers of 2 up through 1024.

Run that, and you’ll get the following result:

2
4
8
16
32
64
128
256
512
1024

Each power of two is printed on a new line. What if you wanted to print the numbers on a single line, though, like so:

2 4 8 16 32 64 128 256 512 1024

The print statement doesn’t allow you to do that directly. However, if you build the string first, you can print it when you’re finished.

Add this modified for loop at the end of main:

final buffer = StringBuffer();
for (int i = 2; i <= 1024; i *= 2) {
  buffer.write(i);
  buffer.write(' ');
}
print(buffer);

In every loop, you write the number to the buffer and add a space. There’s no need to call buffer.toString() in this case because the print statement handles that internally.

Run the code above, and you should see the expected result:

2 4 8 16 32 64 128 256 512 1024

Here are a few more situations where a string buffer will come in handy:

  • Listening to a stream of data coming from the network.
  • Processing a text file one line at a time.
  • Building a string from multiple database queries.

Exercise

Use a string buffer to build the following string:

 *********
* ********
** *******
*** ******
**** *****
***** ****
****** ***
******* **
******** *
*********

Hint: Use a loop inside a loop.

String Validation

  • Hello, I’m a user of your app, and my telephone number is 555732872937482748927348728934723937489274.
  • Hello, I’m a user of your app, and my credit card number is Pizza.
  • Hello, I’m a user of your app, and my address is '; DROP TABLE users; -- .

You should never trust user input. It’s not that everyone is a hacker trying to break into your system — though you need to be on your guard against that, too — it’s just that a lot of the time, innocent users make simple typing mistakes. It’s your job to make sure you only allow data that’s in the proper format.

Verifying that user text input is in the proper form is called string validation. Here are a few common examples of string data you should validate:

  • Telephone numbers
  • Credit card numbers
  • Email addresses
  • Passwords

Even though some of these are “numbers”, you’ll still process them as strings.

Checking the Contents of a String

The String class contains several methods that will help you validate the contents of a string. To demonstrate that functionality, write the following line in main:

const text = 'I love Dart';

You can check whether that string begins with the letter I using startsWith. Add the following line at the end of main:

print(text.startsWith('I')); // true

startsWith returns a Boolean value, which is true in this case. Verify that by running the code.

Similarly, you can use endsWith to check the end of a string:

print(text.endsWith('Dart')); // true

This is also true.

And if you want to check the middle of a string, use contains:

print(text.contains('love'));    // true
print(text.contains('Flutter')); // false

These examples are all very nice, but how would you verify that a phone number contains only numbers or a password contains upper and lowercase letters, numbers and special characters?

One possible solution would be to loop through every character and check whether its code unit value falls within specific Unicode ranges.

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 ! '' # $ % & ( ) * + , - . / ' 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 0 1 2 3 4 5 6 7 8 9 : ; < = > ? 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 @ A B C D E F G H I J K L M N O 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 P Q R S T U V W X Y Z [ \ ] ^ _ 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 ` a b c d e f g h i j k l m n o 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 p q r s t u v w x y z { | } ~ DEL
Unicode characters in the range 32-127

For example, an uppercase letter must fall within the Unicode range of 65-90, a lowercase letter within 97-122, a number within 48-57 and special characters within other ranges, depending on the specific characters you want to allow.

Checking every character like this would be tedious, though. There’s an easier way, which you’ll learn about in the next section.

Regular Expressions

Regular expressions, sometimes called regex, express complex matching patterns in an abbreviated form. Most programming languages support them, and Dart is no exception. Although there are some syntax variations between languages, the differences are minor. Dart shares the same syntax as regular expressions in JavaScript.

Matching Literal Characters

Use the RegExp class to create a regex matching pattern in Dart.

Write the following in main:

final regex = RegExp('cat');

There are a few ways you can use this pattern. One is to call the hasMatch method like so:

print(regex.hasMatch('concatenation'));  // true
print(regex.hasMatch('dog'));            // false
print(regex.hasMatch('cats'));           // true

hasMatch returns true if the regex pattern matches the input string. In this case, both concatenation and cats contain the substring cat, so these return true, whereas dog returns false because it doesn’t match the string literal cat.

An alternative method to accomplish the same task would be to use the contains method on String like you did earlier:

print('concatenation'.contains(regex));  // true
print('dog'.contains(regex));            // false
print('cats'.contains(regex));           // true

The results are the same.

Matching string literals has limited use. The power of regular expressions is in the special characters.

Matching Any Single Character

Regular expressions use special characters that act as wildcards. You can use them to match more than just literal characters.

The . dot character, for example, will match any single character.

Try the following example:

final matchSingle = RegExp('c.t');

print(matchSingle.hasMatch('cat')); // true
print(matchSingle.hasMatch('cot')); // true
print(matchSingle.hasMatch('cut')); // true
print(matchSingle.hasMatch('ct'));  // false

Because the . matches any single character, it will match the a of cat, the o of cot and the u of cut. This gives you much more flexibility in what you match.

The regex pattern c.t didn’t match the string ct because . always matches one character. If you want to also match ct, use the pattern c.?t, where the ? question mark is a special regex character that optionally matches the character before it. Because the previous character is ., the pattern .? matches one or zero instances of any character.

Look at the modified example that uses c.?t:

final optionalSingle = RegExp('c.?t');

print(optionalSingle.hasMatch('cat')); // true
print(optionalSingle.hasMatch('cot')); // true
print(optionalSingle.hasMatch('cut')); // true
print(optionalSingle.hasMatch('ct'));  // true

This time all the inputs match.

Matching Multiple Characters

Two special characters enable you to match more than one character:

  • +: The plus sign means the character it follows can occur one or more times.
  • *: The asterisk means the character it follows can occur zero or more times.

Write the following examples to see how they work:

final oneOrMore = RegExp('wo+w');
print(oneOrMore.hasMatch('ww'));        // false
print(oneOrMore.hasMatch('wow'));       // true
print(oneOrMore.hasMatch('wooow'));     // true
print(oneOrMore.hasMatch('wooooooow')); // true

final zeroOrMore = RegExp('wo*w');
print(zeroOrMore.hasMatch('ww'));        // true
print(zeroOrMore.hasMatch('wow'));       // true
print(zeroOrMore.hasMatch('wooow'));     // true
print(zeroOrMore.hasMatch('wooooooow')); // true

o+ matched o, ooo and ooooooo but not the empty space between the w’s of ww. On the other hand, o* matched everything, even the empty space.

If you want to allow multiple instances of any character, combine . with + or *. Write the following example:

final anyOneOrMore = RegExp('w.+w');

print(anyOneOrMore.hasMatch('ww'));        // false
print(anyOneOrMore.hasMatch('wow'));       // true
print(anyOneOrMore.hasMatch('w123w'));     // true
print(anyOneOrMore.hasMatch('wABCDEFGw')); // true

Here you use the combination .+ to match o, 123 and ABCDEFG.

Matching Sets of Characters

The . regex will match any character, but it’s often useful to match a limited set or range of characters. You can accomplish that using [] square brackets. Only the characters you put inside the square brackets will be used to find a match.

final set = RegExp('b[oa]t');

print(set.hasMatch('bat'));  // true
print(set.hasMatch('bot'));  // true
print(set.hasMatch('but'));  // false
print(set.hasMatch('boat')); // false
print(set.hasMatch('bt'));   // false

The set [ao] matches one a or one o but not both.

You can also specify ranges inside the brackets if you use the - dash character:

final letters = RegExp('[a-zA-Z]');

print(letters.hasMatch('a'));  // true
print(letters.hasMatch('H'));  // true
print(letters.hasMatch('3z')); // true
print(letters.hasMatch('2'));  // false

The regex '[a-zA-Z]' contains two ranges: all of the lowercase letters from a to z and all of the uppercase letters from A to Z. There will be a match as long as the input string has at least one lower or uppercase letter.

If you want to specify which characters not to match, add ^ just after the left bracket:

final excluded = RegExp('b[^ao]t');
print(excluded.hasMatch('bat'));  // false
print(excluded.hasMatch('bot'));  // false
print(excluded.hasMatch('but'));  // true
print(excluded.hasMatch('boat')); // false
print(excluded.hasMatch('bt'));   // false

[^ao] matches one of any character except a or o.

Escaping Special Characters

What if you want to match a special character itself? You can escape it by prefixing the special character with a \ backslash. However, because the backslash is also a special character in Dart strings, it’s usually better to use raw Dart strings whenever you create regular expressions. Do you still remember how to create raw strings in Dart? Prefix them with r, which stands for “raw”.

final escaped = RegExp(r'c\.t');

print(escaped.hasMatch('c.t')); // true
print(escaped.hasMatch('cat')); // false

If you hadn’t prefixed the regex pattern with r, you would have needed to write 'c\\.t' with two backslashes, one to escape the \ special character in Dart and one to escape the . special character in regular expressions.

In the future, this book will always use raw Dart strings for regular expressions. The only reason you wouldn’t is if you needed to insert a Dart variable using interpolation. See Dart Apprentice: Fundamentals, Chapter 4, “Strings”, for a review on string interpolation.

Matching the Beginning and End

If you want to validate that a phone number contains only numbers, you might expect to use the following regular expression:

final numbers = RegExp(r'[0-9]');

This does match the range of numbers from 0 to 9. However, you’ll discover a problem if you try to match the following cases:

print(numbers.hasMatch('5552021')); // true
print(numbers.hasMatch('abcefg2')); // true

That second one shouldn’t be a valid phone number, but it passes your validation check because it does contain the number 2.

What you want is for every character to be a number.

You can use the following regex to accomplish that:

final onlyNumbers = RegExp(r'^[0-9]+$');

print(onlyNumbers.hasMatch('5552021')); // true
print(onlyNumbers.hasMatch('abcefg2')); // false

The regex ^[0-9]+$ is a bit cryptic, so here’s the breakdown:

  • ^: Matches the beginning of the string.
  • [0-9]: Matches one number in the range 0-9.
  • +: Matches one or more instances of the previous character, in this case, one or more numbers in the range 0-9.
  • $: Matches the end of the string.

In summary, the regex ^[0-9]+$ only will match strings that contain numbers from start to end.

Note: The ^ character has two meanings in regex. When used inside [] square brackets, it means “not”. When used elsewhere, it matches the beginning of the line.

Example: Validating a Password

Here’s how you might validate a password where you require the password to contain at least one of each of the following:

  • Lowercase letter.
  • Uppercase letter.
  • Number.

Write the following code in main to demonstrate how this would work:

const password = 'Password1234';

final lowercase = RegExp(r'[a-z]');
final uppercase = RegExp(r'[A-Z]');
final number = RegExp(r'[0-9]');

if (!password.contains(lowercase)) {
  print('Your password must have a lowercase letter!');
} else if (!password.contains(uppercase)) {
  print('Your password must have an uppercase letter!');
} else if (!password.contains(number)) {
  print('Your password must have a number!');
} else {
  print('Your password is OK.');
}

This first checks for lowercase, then uppercase and finally numbers.

Run that to see the following result:

Your password is OK.

You probably noticed that a short password like Pw1 would still work, so you’ll also want to enforce a minimum length. One way to do that would be like so:

if (password.length < 12) {
  print('Your password must be at least 12 characters long!');
}

You could also accomplish the same task by using a regular expression:

final goodLength = RegExp(r'^.{12,}$');

if (!password.contains(goodLength)) {
  print('Your password must be at least 12 characters long!');
}

Recall that ^ and $ match the beginning and end of the string. This ensures you’re validating the whole password. The {} curly braces indicate a length range in regex. Using {12} means a length of exactly 12, {12,15} means a length of 12 to 15 characters, and {12,} means a length of at least 12 with no upper limit. Because {12,} follows the . character, you’re allowing 12 or more of any character.

Note: Although regular expressions are powerful, they’re also notoriously hard to read. When you have a choice, go for the more readable option. In this case, using password.length is perhaps the better choice. But that’s subjective, and the goodLength name is also fairly readable, so you’ll have to make that call.

Regex Summary

The table below summarizes the regular expression special characters you’ve already learned, plus a few more you haven’t:

  • .: Matches one of any character.

  • ?: Zero or one match of the previous character.

  • +: One or more matches of the previous character.

  • *: Zero or more matches of the previous character.

  • {3}: 3 matches of the previous character.

  • {3,5}: 3-5 matches of the previous character.

  • {3,}: 3 or more matches of the previous character.

  • []: Matches one of any character inside the brackets.

  • [^]: Matches one of any character not inside the brackets.

  • \: Escapes the special character that follows.

  • ^: Matches the beginning of a string or line.

  • $: Matches the end of a string or line.

  • \d: Matches one digit.

  • \D: Matches one non-digit.

  • \s: Matches one whitespace character.

  • \S: Matches one non-whitespace character.

  • \w: Matches one alphanumeric character. Same as [a-zA-Z0-9_].

  • \W: Matches one non-alphanumeric character.

  • \uXXXX: Matches a Unicode character where XXXX is the Unicode value.

This list isn’t exhaustive, but it should get you pretty far.

Exercise

Validate that a credit card number contains only numbers and is exactly 16 digits long.

Extracting text

Another common task when manipulating strings is extracting chunks of text from a longer string. You’ll learn two ways to accomplish this, one with substring and another with regex groups.

Extracting Text With Substring

Start with the following simple HTML text document:

<!DOCTYPE html>
<html>
<body>
<h1>Dart Tutorial</h1>
<p>Dart is my favorite language.</p>
</body>
</html>

Finding a Single Match

Say you want to extract the text Dart Tutorial, which is between the <h1> and </h1> tags.

Put the HTML file inside a multiline string like so:

const htmlText = '''
<!DOCTYPE html>
<html>
<body>
<h1>Dart Tutorial</h1>
<p>Dart is my favorite language.</p>
</body>
</html>
''';

Now, extract the desired text by writing the following:

final heading = htmlText.substring(34, 47);
print(heading); // Dart Tutorial

The D of Dart Tutorial is the 34th character in the string, and the final l of Dart Tutorial is the 46th character. The substring method extracts a string between two indexes in a longer string. The start index is inclusive, and the end index is exclusive. Exclusive means the range doesn’t include that index. For example, if you write 47 as the end index, the last character in the range will be at index 46. This might seem strange, but it works out well in a zero-based indexing system where the length of the string is also the end index of the final character.

You’re now probably asking, “How in the world do I know what the index numbers are?” Good question. The indexOf method will help you with that.

Add the following code below what you wrote previously:

final start = htmlText.indexOf('<h1>') + '<h1>'.length; // 34
final end = htmlText.indexOf('</h1>');                  // 47
heading = htmlText.substring(start, end);
print(heading);

Calling indexOf('<h1>') finds where <h1> begins in the text, which turns out to be at index 30. To find the beginning of Dart Tutorial, you need to add the length of the <h1> tag itself, which is 4. Adding 30 and 4 gives the start index of 34. To find the end index, simply search for the closing tag </h1>. Because the end index is exclusive, index 47 is exactly what you want.

Run the code again, and you’ll see the same result.

Finding Multiple Matches

What if there are multiple headers? In that case, you can provide a minimum start index to indexOf as you loop through every match.

Replace main with the following example:

const text = '''
<h1>Dart Tutorial</h1>
<h1>Flutter Tutorial</h1>
<h1>Other Tutorials</h1>
''';

var position = 0;
while (true) {
  var start = text.indexOf('<h1>', position) + '<h1>'.length;
  var end = text.indexOf('</h1>', position);
  if (start == -1 || end == -1) {
    break;
  }
  final heading = text.substring(start, end);
  print(heading);
  position = end + '</h1>'.length;
}

Here, you use position to track where you are in the string. After extracting one match, you move position to after the end index to find the next match on the next loop. indexOf will only find the first match after the specified position. If no match is found, then indexOf will return -1 and you can stop searching.

Run the code, and you’ll see the extracted text:

Dart Tutorial
Flutter Tutorial
Other Tutorials

Extracting Text With Regex Groups

The other way to accomplish the same objective is to use regular expression groups. These are the same regular expressions you used when validating strings. The only thing you need to add is a pair of parentheses around the part you want to extract.

Using the same text as in the last example, add the following code to the end of main:

// 1
final headings = RegExp(r'<h1>(.+)</h1>');
// 2
final matches = headings.allMatches(text);

for (final match in matches) {
  // 3
  print(match.group(1));
}

Here are explanations of the numbered comments:

  1. <h1> and </h1> match literal characters in the text, and .+ matches everything between them. Surrounding .+ with parentheses, as in (.+), marks this text as a regex group.
  2. The original text has three headings that match the regex pattern, so matches will be a collection of three.
  3. group(1) holds the text from the regex group you made earlier using parentheses. This example only used one set of parentheses. If you had used a second set of parentheses, you could access that text using group(2).

Run the code, and you’ll see the text of the three matches printed to the console:

Dart Tutorial
Flutter Tutorial
Other Tutorials

Challenges

You’ve come a long way. Before going on, try out a few challenges to test your string manipulation ability. If you need the answers, you can find them in the supplemental materials accompanying the book.

Challenge 1: Email Validation

Write a regular expression to validate an email address.

Challenge 2: Karaoke Words

An LRC file contains the timestamps for the lyrics of a song. How would you extract the time and lyrics for the following line of text:

[00:12.34]Row, row, row your boat

Extract the relevant parts of the string and print them in the following format:

minutes: 00
seconds: 12
hundredths: 34
lyrics: Row, row, row your boat

Solve the problem twice, once with substring and once with regex groups.

Key Points

  • The String class contains many built-in methods to modify strings, including trim, padLeft, padRight, split, replaceAll and substring.
  • When building a string piece by piece, using StringBuffer is the most efficient.
  • Always validate user input.
  • Regular expressions are a powerful way to match strings to a specified pattern.
  • You can extract strings from text with String.substring or regex groups.

Where to Go From Here?

Regex in Your Editor

Regular expressions are not only useful for Dart code. You can use them in many editors as well. For example, in VS Code, press Command-F on a Mac or Control-F on a PC to show the Find bar. Select the Use Regular Expression button, and then you’ll be able to search powerfully through all of your code:

The example in the image above finds every line that begins with a capital letter.

Combine that with replacement, and you can even use regex groups. Use $1 in the Replacement field to capture the first group from the Find field.

The example in the image above would find something like this:

print(text.startsWith('I'))

And replace it with the following:

text.startsWith('I')

This effectively removes the whole print statement in a single step!

String Validation Packages

Although any serious developer should know how to use regular expressions, you also don’t need to reinvent the wheel when it comes to string validation. Search pub.dev for “string validation” to find packages that probably already do what you need. You can always go to their source code and copy the regex pattern if you don’t want to add another dependency just for a single validation.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.