Last week, we looked at iterators and generators in JavaScript. We learned that we can change an object's default iteration behaviour by adding an [@@iterator]()
method to the object. We also learned that generator functions make this easier. What we didn't cover is that generator functions don't have to be implemented as [@@iterator]()
methods. Let's create some generator functions to help us generate the letters of the alphabet!
Declaring a generator function
To declare a generator function, we append an asterisk (*
) to the function
keyword. We can do this using a function declaration or function expression. The main difference is that function declarations are hoisted while function expressions are not. We cannot declare a generator function using an arrow function expression.
// Function declaration
function* gen() {}
// Function expression
let gen = function* () {};
We're going to declare two generator functions: genAlphaUpper()
and genAlphaLower()
.
Generating the uppercase letters
Let's declare our genAlphaUpper()
generator function. It will have two parameters: start
with a default value of 0
and stop
with a default value of 26
. The first parameter refers the index of the first letter to be included in the sequence; the second parameter refers to the index of the first letter to be excluded from the sequence. This is for parity with the String.prototype.slice()
method. It means we can compute the number of generated letters as stop - start
.
function* genAlphaUpper(start = 0, stop = 26) {}
We'll declare two constants: ALPHABET_LENGTH
with a value of 26
and LATIN_CAPITAL_LETTER_A
with a value of 65
. The former is the number of letters in the alphabet while the latter is the code point for the uppercase letter "A"
. In the Unicode Standard, this is actually represented as a hexadecimal (base-16) number with a U+
prefix: U+0041
. For simplicity, we'll use the equivalent decimal (base-10) number: 65
. If we wanted to be consistent with the Unicode Standard, we could use the hexadecimal number 0x0041
instead (hexadecimal numbers are written with a 0x
prefix in JavaScript).
function* genAlphaUpper(start = 0, stop = 26) {
const ALPHABET_LENGTH = 26;
const LATIN_CAPITAL_LETTER_A = 65;
}
It would be nice if we could pass in negative indices to count backwards from 26
. We'll check if the values of the start
and stop
parameters are less than 0
. If so, we'll add the value of the ALPHABET_LENGTH
constant (26
) to them. For example, if we passed in a start
value of -1
, this would be computed as -1 + 26
, which is equal to 25
. This is the index of the final letter in the alphabet.
function* genAlphaUpper(start = 0, stop = 26) {
const ALPHABET_LENGTH = 26;
const LATIN_CAPITAL_LETTER_A = 65;
if (start < 0) {
start += ALPHABET_LENGTH;
}
if (stop < 0) {
stop += ALPHABET_LENGTH;
}
}
Once we've computed the indices, we need to make sure they're in range. If the value of the start
parameter is less than 0
or the value of the stop
parameter is greater than the value of the ALPHABET_LENGTH
constant (26
), we'll throw a RangeError
.
function* genAlphaUpper(start = 0, stop = 26) {
const ALPHABET_LENGTH = 26;
const LATIN_CAPITAL_LETTER_A = 65;
if (start < 0) {
start += ALPHABET_LENGTH;
}
if (stop < 0) {
stop += ALPHABET_LENGTH;
}
if (start < 0 || stop > ALPHABET_LENGTH) {
throw new RangeError("Indices are out of range.");
}
}
Now we'll add the value of the LATIN_CAPITAL_LETTER_A
constant (65
) to the values of the start
and stop
parameters. In the Unicode Standard, the code points for the uppercase letters "A"
to "Z"
are U+0041
(65
) to U+005A
(90
). Adding 65
takes us to the code points in this range.
function* genAlphaUpper(start = 0, stop = 26) {
const ALPHABET_LENGTH = 26;
const LATIN_CAPITAL_LETTER_A = 65;
if (start < 0) {
start += ALPHABET_LENGTH;
}
if (stop < 0) {
stop += ALPHABET_LENGTH;
}
if (start < 0 || stop > ALPHABET_LENGTH) {
throw new RangeError("Indices are out of range.");
}
// In the Unicode Standard, the code points for the uppercase letters A to Z
// are the hexadecimal numbers U+0041 (65) to U+005A (90).
start += LATIN_CAPITAL_LETTER_A;
stop += LATIN_CAPITAL_LETTER_A;
}
Finally, we'll use a simple for
loop to yield
the return value of the static String.fromCodePoint()
method on each invocation of the generator function.
function* genAlphaUpper(start = 0, stop = 26) {
const ALPHABET_LENGTH = 26;
const LATIN_CAPITAL_LETTER_A = 65;
if (start < 0) {
start += ALPHABET_LENGTH;
}
if (stop < 0) {
stop += ALPHABET_LENGTH;
}
if (start < 0 || stop > ALPHABET_LENGTH) {
throw new RangeError("Indices are out of range.");
}
// In the Unicode Standard, the code points for the uppercase letters A to Z
// are the hexadecimal numbers U+0041 (65) to U+005A (90).
start += LATIN_CAPITAL_LETTER_A;
stop += LATIN_CAPITAL_LETTER_A;
for (let i = start; i < stop; i++) {
yield String.fromCodePoint(i);
}
}
Generating the lowercase letters
Our genAlphaLower()
function will be almost identical, except we need to use the code points U+0061
(97
) to U+007A
(122
). In the Unicode Standard, these represent the lowercase letters "a"
to "z"
. We'll use the constant LATIN_SMALL_LETTER_A
instead of LATIN_CAPITAL_LETTER_A
.
function* genAlphaLower(start = 0, stop = 26) {
const ALPHABET_LENGTH = 26;
const LATIN_SMALL_LETTER_A = 97;
if (start < 0) {
start += ALPHABET_LENGTH;
}
if (stop < 0) {
stop += ALPHABET_LENGTH;
}
if (start < 0 || stop > ALPHABET_LENGTH) {
throw new RangeError("Indices are out of range.");
}
// In the Unicode Standard, the code points for the lowercase letters a to z
// are the hexadecimal numbers U+0061 (97) to U+007A (122).
start += LATIN_SMALL_LETTER_A;
stop += LATIN_SMALL_LETTER_A;
for (let i = start; i < stop; i++) {
yield String.fromCodePoint(i);
}
}
Using the generator functions
Generator functions return Generator
objects. These are a type of iterable iterator. Because of this, we can use the returned Generator
object with syntaxes that expect an iterable. This includes spread syntax (...
) and the for...of
statement.
Creating an array
To create an array of the uppercase letters "A"
to "Z"
, we can use spread syntax to iterate over the returned Generator
object. Note that we could also use the Array.from()
method.
let alphabet = [...genAlphaUpper()];
Looping through the values
To loop through the uppercase letters "A"
to "Z"
, we can use a for...of
loop to iterate over the returned Generator
object.
for (let letter of genAlphaUpper()) {
console.log(letter);
}
Generating a subset
Because our generator functions accept start
and stop
indices, we can generate a subset of the alphabet. This is more memory-efficient than generating the entire alphabet and slicing it.
let eToG = [...genAlphaUpper(4, 7)];
If the indices don't make sense, e.g. if we try to start
at index 12
and stop
at index 9
, the generator functions won't yield
anything. This is because the conditions of the for
loops will never be truthy, so the for
loops will never execute.
Working with the Generator
object
For more control, we can work with the Generator
object directly. For example, we can invoke the Generator.prototype.next()
method manually.
let gen = genAlphaUpper();
let done = false;
while (!done) {
let result = gen.next();
if (typeof result.value !== "undefined") {
console.log(result.value);
}
done = result.done;
}
Summary
In this post, we declared two generator functions: genAlphaUpper()
and genAlphaLower()
. Respectively, they use the static String.fromCodePoint()
method to generate the alphabet in uppercase and lowercase. Because they accept start
and stop
indices, they can generate subsets of the alphabet instead of forcing us to generate the entire alphabet and slice it. This is more memory-efficient.