Learn Practice Daily golf Reference
Navigation·Lesson 4 of 5·7 min·0/6 exercisesPro

Matching pairs

% jumps from one end of a pair of brackets to the other, across lines too; off a bracket it uses the next one on the line, and d% deletes as far as the partner. Against di( and da( on the same line.

Scene

Wednesday, 3:20 a.m. The fix is in, and deliveryFee ends in Math.round((base + perKm + rush) * 100) / 100: brackets inside brackets, on a line you're reading half asleep. Before you deploy, you want to be sure each one closes where you think it does.

Every bracket has a partner, and Vim knows which.

From memory

Warm-up from earlier chapters

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

Back to the last place you changed something.
press the keys…2 keys
1 of 2
Back to the last place you changed something.

One end to the other

Every pair of brackets is a layer of packaging with two ends, like the foil, the bag and the box around a delivery. On a bracket, % jumps to the other end of its layer: from ( to its ), from { to its }, even when that's thirty lines down. Press it again and you're back.

return Math.round((base + perKm + rush) * 100) / 100;%
From the first ( of round(: % to its partner and back.
1function placeOrder(cart, user, now) {
1 const order = { id: newId(), items: cart.items, address: user.address, placedAt: now.toISOString() };
2 const fee = deliveryFee(order, distanceKm(KITCHEN, order.address));
3 const problem = validateOrder(order);
4 if (problem) throw new Error(problem);
5 order.discount = studentDiscount(order, user);
6 order.total = totalFor(order) - order.discount + fee;
7 return order;
8}
9
10function deliveryFee(order, km) {
11 const base = km <= 2 ? 0 : 1.5;
12 const perKm = km > 2 ? (km - 2) * 0.4 : 0;
13 const rush = isLunchRush(new Date()) ? 1 : 0;
14 return Math.round((base + perKm + rush) * 100) / 100;
15}
NORMALLoading the editor…1:38
Try it

Where functions end

Go to the { at the end of the placeOrder line, then % to the function's closing }. The same for deliveryFee.

1 of 4Move the cursor onto the on line 156, 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
45function validateOrder(order) {
46 if (!order.items || order.items.length === 0) return "empty";
47 if (order.items.some((i) => i.qty <= 0)) return "bad quantity";
48 if (!order.address) return "no address";
49 if (!isOpen(new Date())) return "closed";
50 return null;
51}
52
53function applyCoupon(order, code) {
54 const coupon = COUPONS[code];
55 if (!coupon) return order;
56 if (coupon.expires < Date.now()) return order;
57 const off = Math.round(subtotal(order.items) * coupon.percent);
58 return { ...order, discount: off };
59}
60
61function studentDiscount(order, user) {
62 if (!user.isStudent) return 0;
63 return Math.round(subtotal(order.items) * 0.1);
64}
65
66function distanceKm(a, b) {
67 const dLat = (b.lat - a.lat) * 111;
68 const dLng = (b.lng - a.lng) * 111 * Math.cos((a.lat * Math.PI) / 180);
69 return Math.sqrt(dLat * dLat + dLng * dLng);
70}
71
72function nearestRider(riders, address) {
73 let best = null;
74 for (const rider of riders) {
75 if (!rider.online) continue;
76 const d = distanceKm(rider.position, address.position);
77 if (d > MAX_DISTANCE_KM) continue;
78 if (!best || d < best.distance) best = { rider, distance: d };
79 }
80 return best;
81}
82
83function assignRider(order, riders) {
84 const match = nearestRider(riders, order.address);
85 if (!match) return refund(order, "No rider nearby, sorry!");
86 order.riderId = match.rider.id;
87 order.eta = Math.ceil(match.distance * 4) + 10;
88 notify(match.rider.phone, "New order: " + order.id);
89 return order;
90}
91
92function etaMinutes(order, now) {
93 const rush = isLunchRush(now) ? 10 : 0;
94 return order.eta + rush;
95}
96
97function refund(order, reason) {
98 order.status = "refunded";
99 order.refundReason = reason;
100 payments.refund(order.paymentId, totalFor(order));
101 notify(order.customerPhone, "Refunded: " + reason);
102 return order;
103}
104
105function markDelivered(order, now) {
106 order.status = "delivered";
107 order.deliveredAt = now.toISOString();
108 const late = now - new Date(order.placedAt) > order.eta * 60000;
109 if (late) order.lateBy = Math.round((now - new Date(order.placedAt)) / 60000 - order.eta);
110 return order;
111}
112
113function kitchenTicket(order) {
114 const lines = order.items.map((i) => i.qty + " x " + i.name);
115 lines.unshift("ORDER " + order.id);
116 if (order.notes) lines.push("NOTE: " + order.notes);
117 return lines.join("\n");
118}
119
120function printTicket(order) {
121 const ticket = kitchenTicket(order);
122 printer.print(ticket);
123 log("printed", order.id);
124}
125
126function emailReceipt(order, user) {
127 const body = renderReceipt(order);
128 mailer.send(user.email, "Your Byte Burrito receipt", body);
129}
130
131function menuItem(id) {
132 const item = MENU.find((m) => m.id === id);
133 if (!item) throw new Error("not on the menu: " + id);
134 return item;
135}
136
137function isSpicy(id) {
138 return menuItem(id).spicy === true;
139}
140
141function happyHourPrice(item, now) {
142 const hour = now.getHours();
143 if (hour >= 17 && hour < 19) return Math.round(item.price * 0.8);
144 return item.price;
145}
146
147function priceList(now) {
148 return MENU.map((item) => ({
149 id: item.id,
150 name: item.name,
151 price: formatPrice(happyHourPrice(item, now)),
152 }));
153}
154
155function placeOrder(cart, user, now) {
156 const order = { id: newId(), items: cart.items, address: user.address, placedAt: now.toISOString() };
157 const fee = deliveryFee(order, distanceKm(KITCHEN, order.address));
158 const problem = validateOrder(order);
159 if (problem) throw new Error(problem);
160 order.discount = studentDiscount(order, user);
161 order.total = totalFor(order) - order.discount + fee;
162 return order;
163}
164
165function deliveryFee(order, km) {
166 const base = km <= 2 ? 0 : 1.5;
167 const perKm = km > 2 ? (km - 2) * 0.4 : 0;
168 const rush = isLunchRush(new Date()) ? 1 : 0;
169 return Math.round((base + perKm + rush) * 100) / 100;
170}
171
172function newId() {
173 return "BB-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
174}
175
176function log(event, ...details) {
177 console.log("[" + new Date().toISOString() + "] " + event, ...details);
178}
179
180function notify(phone, text) {
181 if (!phone) return;
182 sms.send(phone, text.slice(0, 160));
183}
184
185function saveUser(user) {
186 db.users.put(user.id, user);
187}
188
189function loadUser(id) {
190 return db.users.get(id);
191}
192
193function saveOrder(order) {
194 db.orders.put(order.id, order);
195 log("saved", order.id);
196}
197
198function loadOrder(id) {
199 return db.orders.get(id);
200}
201
202function ordersToday(now) {
203 const day = now.toISOString().slice(0, 10);
204 return db.orders.all().filter((o) => o.placedAt.startsWith(day));
205}
206
207function revenueToday(now) {
208 return ordersToday(now).reduce((sum, o) => sum + o.total, 0);
209}
210
211function busiestHour(now) {
212 const counts = new Array(24).fill(0);
213 for (const o of ordersToday(now)) counts[new Date(o.placedAt).getHours()]++;
214 return counts.indexOf(Math.max(...counts));
215}
216
217function ridersOnline(riders) {
218 return riders.filter((r) => r.online).length;
219}
220
221function riderReport(riders, now) {
222 const online = ridersOnline(riders);
223 const rush = isLunchRush(now);
224 return { online, rush, short: rush && online < 6 };
225}
226
227function weatherDelay(weather) {
228 if (weather === "rain") return 8;
229 if (weather === "snow") return 20;
230 return 0;
231}
232
233function adjustEta(order, weather) {
234 order.eta = order.eta + weatherDelay(weather);
235 return order;
236}
237
238function cancelOrder(order, reason) {
239 if (order.status === "delivered") return order;
240 order.status = "cancelled";
241 return refund(order, reason);
242}
243
244function reorder(previous, user, now) {
245 const cart = { items: previous.items.map((i) => ({ ...i })) };
246 return placeOrder(cart, user, now);
247}
248
249function favourite(user) {
250 const counts = {};
251 for (const o of db.orders.byUser(user.id)) {
252 for (const i of o.items) counts[i.id] = (counts[i.id] || 0) + i.qty;
253 }
254 return Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0];
255}
256
257module.exports = {
258 isOpen,
259 placeOrder,
260 assignRider,
261 markDelivered,
262 printTicket,
263 emailReceipt,
264 priceList,
265 revenueToday,
266 riderReport,
267 cancelOrder,
268 reorder,
269};
NORMALLoading the editor…1/40.0s1:1

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

Not on a bracket?

If the cursor isn't on a bracket, % looks along the line for the first bracket after the cursor and jumps to that one's partner. So from inside a layer, when the next bracket along is the one that closes it, % takes you to the layer's opening.

Check yourself

From inside

1 return Math.round((base + perKm + rush) * 100) / 100;
NORMAL
Keys pressed: %

The cursor is on rush, not on a bracket. You press %. Where does it go?

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

A whole layer

% is a motion, so verbs take it: with the cursor on a bracket, d% deletes the pair and everything in it, y% copies it, v% highlights it so you can see the whole layer before deciding.

%
return Math.round((base + perKm + rush) * 100) / 100;
i(
return Math.round((base + perKm + rush) * 100) / 100;
a(
return Math.round((base + perKm + rush) * 100) / 100;
One cursor inside the sum. d% runs from the cursor to the other end of the layer, so it takes half of it; di( and da( take the layer you're in, wherever you are inside it. From inside, use the layer; % is for travelling.
Try it

Tidy the layers

Before you deploy, two leftovers in deliveryFee. On the perKm line someone doubled the brackets around the sum: from the outer (, % to its partner, x, `` back to where the jump started (track 11's exact-spot ribbon), x. And the debug log no longer needs its last argument: from the comma before it, 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.

Try it

Pairs in the code

Targets on brackets across the codebase. % is often the shortest way there.

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

Pairs, from memory

Press the keys.

Jump to the partner of this bracket.
scroll here, then press the keys
1 of 3
Jump to the partner of this bracket.