Learn Practice Daily golf Reference
Vertical movement·Lesson 3 of 6·10 min·0/10 exercisesPro

Paragraphs

Blank lines are paragraph breaks: } and { jump to the next and previous one, and d} cuts a whole function from the break above it.

Scene

Thursday, 4:30 p.m. Turning pages in legacy.js all afternoon, you started to see its shape: every function is a block of lines with a blank line before and after. Constants together, then one function, a gap, the next function, a gap.

Paragraphs, in other words. And Vim reads code in paragraphs too.

From memory

Warm-up from earlier chapters

Two from earlier chapters, from memory, before the new idea. Two wrong tries show the answer.

To the last line of the file.
press the keys…
1 of 2
To the last line of the file.

Paragraph breaks

To Vim, a paragraph is a run of lines with something on them, and a blank line is the break between paragraphs. } jumps forward to the next break, and { jumps back to the previous one. In code that means one key from function to function, however long each one is.

}
function totalFor(order) {
const base = subtotal(order.items);
const tax = taxFor(base);
const tip = order.tip || 0;
return base + tax + tip;
}
// DEAD CODE: the fax ordering line was switched off in 2020
function faxOrder(order) {
const sheet = renderFaxSheet(order);
faxMachine.send(sheet, "+1-555-0100");
log("fax sent", order.id);
return true;
}
{
function totalFor(order) {
const base = subtotal(order.items);
const tax = taxFor(base);
const tip = order.tip || 0;
return base + tax + tip;
}
// DEAD CODE: the fax ordering line was switched off in 2020
function faxOrder(order) {
const sheet = renderFaxSheet(order);
faxMachine.send(sheet, "+1-555-0100");
log("fax sent", order.id);
return true;
}
2}
function totalFor(order) {
const base = subtotal(order.items);
const tax = taxFor(base);
const tip = order.tip || 0;
return base + tax + tip;
}
// DEAD CODE: the fax ordering line was switched off in 2020
function faxOrder(order) {
const sheet = renderFaxSheet(order);
faxMachine.send(sheet, "+1-555-0100");
log("fax sent", order.id);
return true;
}
One press each from inside a function: } to the break after it, { to the break before, 2} two breaks on.
1// legacy.js — Byte Burrito, 2019–present. Please don't add anything here.
1// (Everyone has added something here.)
2
3const TAX_RATE = 0.0875;
4const MAX_DISTANCE_KM = 8;
5const OPEN_HOUR = 11;
6const CLOSE_HOUR = 23;
7const RUSH_START = 12;
8const RUSH_END = 14;
9
10function isOpen(now) {
11 const hour = now.getHours();
12 return hour >= OPEN_HOUR && hour < CLOSE_HOUR;
13}
14
15function isLunchRush(now) {
16 const hour = now.getHours();
17 return hour >= RUSH_START && hour < RUSH_END;
18}
19
20function formatPrice(cents) {
21 const dollars = Math.floor(cents / 100);
22 const rest = String(cents % 100).padStart(2, "0");
23 return "$" + dollars + "." + rest;
24}
25
26function subtotal(items) {
27 let sum = 0;
28 for (const item of items) {
29 sum += item.price * item.qty;
30 }
31 return sum;
32}
33
34function taxFor(amount) {
35 return Math.round(amount * TAX_RATE);
36}
37
38function totalFor(order) {
39 const base = subtotal(order.items);
40 const tax = taxFor(base);
41 const tip = order.tip || 0;
42 return base + tax + tip;
43}
44
45// DEAD CODE: the fax ordering line was switched off in 2020
46function faxOrder(order) {
47 const sheet = renderFaxSheet(order);
48 faxMachine.send(sheet, "+1-555-0100");
49 log("fax sent", order.id);
NORMALLoading the editor…1:1
Try it

Hop the breaks

Targets sit on the first line of a function. } lands on the break before it, so it's } and then j; { for the ones behind you.

1 of 6Move the cursor onto the on line 299, below the screen
1// legacy.js — Byte Burrito, 2019–present. Please don't add anything here.
1// (Everyone has added something here.)
2
3const TAX_RATE = 0.0875;
4const MAX_DISTANCE_KM = 8;
5const OPEN_HOUR = 11;
6const CLOSE_HOUR = 23;
7const RUSH_START = 12;
8const RUSH_END = 14;
9
10function isOpen(now) {
11 const hour = now.getHours();
12 return hour >= OPEN_HOUR && hour < CLOSE_HOUR;
13}
14
15function isLunchRush(now) {
16 const hour = now.getHours();
17 return hour >= RUSH_START && hour < RUSH_END;
18}
19
20function formatPrice(cents) {
21 const dollars = Math.floor(cents / 100);
22 const rest = String(cents % 100).padStart(2, "0");
23 return "$" + dollars + "." + rest;
24}
25
26function subtotal(items) {
27 let sum = 0;
28 for (const item of items) {
29 sum += item.price * item.qty;
30 }
31 return sum;
32}
33
34function taxFor(amount) {
35 return Math.round(amount * TAX_RATE);
36}
37
38function totalFor(order) {
39 const base = subtotal(order.items);
40 const tax = taxFor(base);
41 const tip = order.tip || 0;
42 return base + tax + tip;
43}
44
45// DEAD CODE: the fax ordering line was switched off in 2020
46function faxOrder(order) {
47 const sheet = renderFaxSheet(order);
48 faxMachine.send(sheet, "+1-555-0100");
49 log("fax sent", order.id);
50 return true;
51}
52
53function validateOrder(order) {
54 if (!order.items || order.items.length === 0) return "empty";
55 if (order.items.some((i) => i.qty <= 0)) return "bad quantity";
56 if (!order.address) return "no address";
57 if (!isOpen(new Date())) return "closed";
58 return null;
59}
60
61function applyCoupon(order, code) {
62 const coupon = COUPONS[code];
63 if (!coupon) return order;
64 if (coupon.expires < Date.now()) return order;
65 const off = Math.round(subtotal(order.items) * coupon.percent);
66 return { ...order, discount: off };
67}
68
69function studentDiscount(order, user) {
70 if (!user.isStudent) return 0;
71 return Math.round(subtotal(order.items) * 0.1);
72}
73
74// DEAD CODE: loyalty points were replaced by the app in 2021
75function addLoyaltyPoints(user, order) {
76 const points = Math.floor(totalFor(order) / 100);
77 user.points = (user.points || 0) + points;
78 saveUser(user);
79 return points;
80}
81
82function distanceKm(a, b) {
83 const dLat = (b.lat - a.lat) * 111;
84 const dLng = (b.lng - a.lng) * 111 * Math.cos((a.lat * Math.PI) / 180);
85 return Math.sqrt(dLat * dLat + dLng * dLng);
86}
87
88function nearestRider(riders, address) {
89 let best = null;
90 for (const rider of riders) {
91 if (!rider.online) continue;
92 const d = distanceKm(rider.position, address.position);
93 if (d > MAX_DISTANCE_KM) continue;
94 if (!best || d < best.distance) best = { rider, distance: d };
95 }
96 return best;
97}
98
99function assignRider(order, riders) {
100 const match = nearestRider(riders, order.address);
101 if (!match) return refund(order, "No rider nearby, sorry!");
102 order.riderId = match.rider.id;
103 order.eta = Math.ceil(match.distance * 4) + 10;
104 notify(match.rider.phone, "New order: " + order.id);
105 return order;
106}
107
108function etaMinutes(order, now) {
109 const rush = isLunchRush(now) ? 10 : 0;
110 return order.eta + rush;
111}
112
113// DEAD CODE: the pager integration, before the riders had phones
114function pageRider(rider, order) {
115 const message = "ORDER " + order.id + " " + order.address.street;
116 pager.send(rider.pagerId, message.slice(0, 80));
117 return true;
118}
119
120function refund(order, reason) {
121 order.status = "refunded";
122 order.refundReason = reason;
123 payments.refund(order.paymentId, totalFor(order));
124 notify(order.customerPhone, "Refunded: " + reason);
125 return order;
126}
127
128function markDelivered(order, now) {
129 order.status = "delivered";
130 order.deliveredAt = now.toISOString();
131 const late = now - new Date(order.placedAt) > order.eta * 60000;
132 if (late) order.lateBy = Math.round((now - new Date(order.placedAt)) / 60000 - order.eta);
133 return order;
134}
135
136function kitchenTicket(order) {
137 const lines = order.items.map((i) => i.qty + " x " + i.name);
138 lines.unshift("ORDER " + order.id);
139 if (order.notes) lines.push("NOTE: " + order.notes);
140 return lines.join("\n");
141}
142
143function printTicket(order) {
144 const ticket = kitchenTicket(order);
145 printer.print(ticket);
146 log("printed", order.id);
147}
148
149// DEAD CODE: the old receipt printer, replaced by email receipts
150function printReceipt(order) {
151 const lines = order.items.map((i) => i.name + " " + formatPrice(i.price * i.qty));
152 lines.push("TOTAL " + formatPrice(totalFor(order)));
153 receiptPrinter.print(lines.join("\n"));
154}
155
156function emailReceipt(order, user) {
157 const body = renderReceipt(order);
158 mailer.send(user.email, "Your Byte Burrito receipt", body);
159}
160
161function menuItem(id) {
162 const item = MENU.find((m) => m.id === id);
163 if (!item) throw new Error("not on the menu: " + id);
164 return item;
165}
166
167function isSpicy(id) {
168 return menuItem(id).spicy === true;
169}
170
171function happyHourPrice(item, now) {
172 const hour = now.getHours();
173 if (hour >= 17 && hour < 19) return Math.round(item.price * 0.8);
174 return item.price;
175}
176
177function priceList(now) {
178 return MENU.map((item) => ({
179 id: item.id,
180 name: item.name,
181 price: formatPrice(happyHourPrice(item, now)),
182 }));
183}
184
185// DEAD CODE: the 2019 "burrito of the month", never shipped
186function burritoOfTheMonth(month) {
187 const picks = ["al-pastor", "veggie", "carnitas", "birria"];
188 return menuItem(picks[month % picks.length]);
189}
190
191function placeOrder(cart, user, now) {
192 const order = { id: newId(), items: cart.items, address: user.address, placedAt: now.toISOString() };
193 const fee = deliveryFee(order, distanceKm(KITCHEN, order.address));
194 const problem = validateOrder(order);
195 if (problem) throw new Error(problem);
196 order.discount = studentDiscount(order, user);
197 order.total = totalFor(order) - order.discount + fee;
198 return order;
199}
200
201function deliveryFee(order, km) {
202 const base = km <= 2 ? 0 : 1.5;
203 const perKm = km > 2 ? (km - 2) * 0.4 : 0;
204 const rush = isLunchRush(new Date()) ? 1 : 0;
205 return Math.round((base + perKm + rush) * 100) / 100;
206}
207
208function newId() {
209 return "BB-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
210}
211
212function log(event, ...details) {
213 console.log("[" + new Date().toISOString() + "] " + event, ...details);
214}
215
216function notify(phone, text) {
217 if (!phone) return;
218 sms.send(phone, text.slice(0, 160));
219}
220
221function saveUser(user) {
222 db.users.put(user.id, user);
223}
224
225function loadUser(id) {
226 return db.users.get(id);
227}
228
229function saveOrder(order) {
230 db.orders.put(order.id, order);
231 log("saved", order.id);
232}
233
234function loadOrder(id) {
235 return db.orders.get(id);
236}
237
238function ordersToday(now) {
239 const day = now.toISOString().slice(0, 10);
240 return db.orders.all().filter((o) => o.placedAt.startsWith(day));
241}
242
243function revenueToday(now) {
244 return ordersToday(now).reduce((sum, o) => sum + o.total, 0);
245}
246
247function busiestHour(now) {
248 const counts = new Array(24).fill(0);
249 for (const o of ordersToday(now)) counts[new Date(o.placedAt).getHours()]++;
250 return counts.indexOf(Math.max(...counts));
251}
252
253// DEAD CODE: the CSV export the accountant asked for, then never used
254function exportCsv(orders) {
255 const rows = orders.map((o) => [o.id, o.total, o.status].join(","));
256 rows.unshift("id,total,status");
257 return rows.join("\n");
258}
259
260function ridersOnline(riders) {
261 return riders.filter((r) => r.online).length;
262}
263
264function riderReport(riders, now) {
265 const online = ridersOnline(riders);
266 const rush = isLunchRush(now);
267 return { online, rush, short: rush && online < 6 };
268}
269
270function weatherDelay(weather) {
271 if (weather === "rain") return 8;
272 if (weather === "snow") return 20;
273 return 0;
274}
275
276function adjustEta(order, weather) {
277 order.eta = order.eta + weatherDelay(weather);
278 return order;
279}
280
281function cancelOrder(order, reason) {
282 if (order.status === "delivered") return order;
283 order.status = "cancelled";
284 return refund(order, reason);
285}
286
287function reorder(previous, user, now) {
288 const cart = { items: previous.items.map((i) => ({ ...i })) };
289 return placeOrder(cart, user, now);
290}
291
292function favourite(user) {
293 const counts = {};
294 for (const o of db.orders.byUser(user.id)) {
295 for (const i of o.items) counts[i.id] = (counts[i.id] || 0) + i.qty;
296 }
297 return Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0];
298}
299
300module.exports = {
301 isOpen,
302 placeOrder,
303 assignRider,
304 markDelivered,
305 printTicket,
306 emailReceipt,
307 priceList,
308 revenueToday,
309 riderReport,
310 cancelOrder,
311 reorder,
312};
NORMALLoading the editor…1/60.0s1:1

This exercise needs a keyboard. Read on, and play it at your computer.

Check yourself

Where does } stop?

1function totalFor(order) {
2 const base = subtotal(order.items);
1 const tax = taxFor(base);
2 const tip = order.tip || 0;
3 return base + tax + tip;
4}
5
6// DEAD CODE: the fax ordering line was switched off in 2020
7function faxOrder(order) {
8 const sheet = renderFaxSheet(order);
9 faxMachine.send(sheet, "+1-555-0100");
10 log("fax sent", order.id);
11 return true;
12}
NORMAL
Keys pressed: }

The cursor is inside totalFor. You press }. Where does it land?

Pick an answer (or press its number), then watch the engine prove it.

} is a noun

Every motion is a noun, and } is a big one: to the next break. From the blank line above a function, d} reads delete to the next break and takes the whole function, leaving a single blank line where it was. One sentence per dead function.

}
function totalFor(order) {
const base = subtotal(order.items);
const tax = taxFor(base);
const tip = order.tip || 0;
return base + tax + tip;
}
// DEAD CODE: the fax ordering line was switched off in 2020
function faxOrder(order) {
const sheet = renderFaxSheet(order);
faxMachine.send(sheet, "+1-555-0100");
log("fax sent", order.id);
return true;
}
function validateOrder(order) {
if (!order.items || order.items.length === 0) return "empty";
if (order.items.some((i) => i.qty <= 0)) return "bad quantity";
if (!order.address) return "no address";
if (!isOpen(new Date())) return "closed";
return null;
}
function applyCoupon(order, code) {
const coupon = COUPONS[code];
if (!coupon) return order;
if (coupon.expires < Date.now()) return order;
const off = Math.round(subtotal(order.items) * coupon.percent);
return { ...order, discount: off };
}
function studentDiscount(order, user) {
if (!user.isStudent) return 0;
return Math.round(subtotal(order.items) * 0.1);
}
// DEAD CODE: loyalty points were replaced by the app in 2021
function addLoyaltyPoints(user, order) {
const points = Math.floor(totalFor(order) / 100);
user.points = (user.points || 0) + points;
saveUser(user);
return points;
}
d} from the break above the fax function: the comment and the whole function, up to the next break.
Try it

Two dead paragraphs

Two functions marked dead. Get onto the blank line above each one, then d}.

Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.

Check yourself

Is it a break?

1const OPEN_HOUR = 11;
1const CLOSE_HOUR = 23;
2
3const RUSH_START = 12;
4const RUSH_END = 14;
5
6function isOpen(now) {
7 const hour = now.getHours();
8 return hour >= OPEN_HOUR && hour < CLOSE_HOUR;
9}
NORMAL
Keys pressed: }

Line 3 looks empty, but it holds four spaces. Line 6 is truly empty. From line 1 you press }. Where does the cursor land?

Pick an answer (or press its number), then watch the engine prove it.
Try it

The gap that isn't

Same constants. Empty line 3 (the editor shows its four spaces as the part to delete), then check from the top that } stops there now.

Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.

A paragraph is layout, not a function

d} cuts to the next blank line, whatever the code means. In legacy.js every function happens to be one paragraph. If a function has a blank line inside it, that's a break too, and d} stops halfway through. Before you cut, press } on its own and see where the break is.

}
function taxFor(amount) {
return Math.round(amount * TAX_RATE);
}
// DEAD CODE: the tip jar on the old kiosk
function tipJar(order) {
const options = [1, 2, 3];
return options.map((n) => order.total + n * 100);
}
function totalFor(order) {
const base = subtotal(order.items);
return base + taxFor(base);
}
2}
function taxFor(amount) {
return Math.round(amount * TAX_RATE);
}
// DEAD CODE: the tip jar on the old kiosk
function tipJar(order) {
const options = [1, 2, 3];
return options.map((n) => order.total + n * 100);
}
function totalFor(order) {
const base = subtotal(order.items);
return base + taxFor(base);
}
From the break above tipJar: d} stops at the blank line inside it; d2} reaches the break after it.
Try it

Halfway through

A different file, the old kiosk code. Someone already pressed d} from the break above tipJar, and only half the function went. Undo it with u, then cut the whole function with a count: two breaks on.

Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.

From memory

Paragraphs, from memory

Press the keys.

Jump to the next blank line.
scroll here, then press the keys
1 of 4
Jump to the next blank line.
Challenge

Function hopping

Twelve function starts all over legacy.js. Breaks, counts, page numbers: whatever gets there first.

Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.

Boss round

Three dead paragraphs

The first three dead functions in legacy.js, top to bottom: the fax line, the loyalty points and the pager. Cut each one out, comment and all, leaving one blank line between the functions around it. The other three can wait until tomorrow.

Stuck? Show a hint
Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.

Extra practice optional

The chapter is done. These rounds keep going as long as you like: stop whenever it stops being useful.

Endless

Paragraph hopping

Endless function starts across the file.

Pro
The rest of this lesson is in Pro

The first exercise of every lesson is free. Pro unlocks the rest, every track, for good.

See Pro

This exercise needs a keyboard. Read on, and play it at your computer.