Learn Practice Daily golf Reference
Vertical movement·Lesson 4 of 6·9 min·0/7 exercisesPro

Scrolling

Turn pages without a scroll wheel: Ctrl-d and Ctrl-u half a page, Ctrl-f and Ctrl-b a whole one, and zz zt zb slide the page around your line.

Scene

Friday, 9:30 a.m. Code review. The whole team is watching your screen while you read legacy.js from top to bottom, 290 lines, and your scroll wheel is in the drawer with the mouse.

Holding j down would take a minute and make everyone seasick. Books have a better way: you turn the page.

From memory

Warm-up from earlier chapters

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

From the blank line above a function, delete the whole function.
press the keys…2 keys
1 of 2
From the blank line above a function, delete the whole function.

Turn the page

Ctrl-f turns a whole page forward and Ctrl-b turns one back. The new page keeps two lines of the old one at the edge, so you never lose your place, and the cursor comes along onto the new page.

Half a page

Ctrl-d and Ctrl-u turn half a page, down or up, and move the cursor by the same number of lines. Half a page is the reading speed: your eyes stay on the code that's still on screen while the next half slides in. Watch the strip on the right: it's where the open page sits in the whole book.

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
126// DEAD CODE: the old receipt printer, replaced by email receipts
127function printReceipt(order) {
128 const lines = order.items.map((i) => i.name + " " + formatPrice(i.price * i.qty));
129 lines.push("TOTAL " + formatPrice(totalFor(order)));
130 receiptPrinter.print(lines.join("\n"));
131}
132
133function emailReceipt(order, user) {
134 const body = renderReceipt(order);
135 mailer.send(user.email, "Your Byte Burrito receipt", body);
136}
137
138function menuItem(id) {
139 const item = MENU.find((m) => m.id === id);
140 if (!item) throw new Error("not on the menu: " + id);
141 return item;
142}
143
144function isSpicy(id) {
145 return menuItem(id).spicy === true;
146}
147
148function happyHourPrice(item, now) {
149 const hour = now.getHours();
150 if (hour >= 17 && hour < 19) return Math.round(item.price * 0.8);
151 return item.price;
152}
153
154function priceList(now) {
155 return MENU.map((item) => ({
156 id: item.id,
157 name: item.name,
158 price: formatPrice(happyHourPrice(item, now)),
159 }));
160}
161
162// DEAD CODE: the 2019 "burrito of the month", never shipped
163function burritoOfTheMonth(month) {
164 const picks = ["al-pastor", "veggie", "carnitas", "birria"];
165 return menuItem(picks[month % picks.length]);
166}
167
168function placeOrder(cart, user, now) {
169 const order = { id: newId(), items: cart.items, address: user.address, placedAt: now.toISOString() };
170 const problem = validateOrder(order);
171 if (problem) throw new Error(problem);
172 const fee = deliveryFee(order, distanceKm(KITCHEN, order.address));
173 order.discount = studentDiscount(order, user);
174 order.total = totalFor(order) - order.discount + fee;
175 return order;
176}
177
178function deliveryFee(order, km) {
179 const base = km <= 2 ? 0 : 1.5;
180 const perKm = km > 2 ? (km - 2) * 0.4 : 0;
181 const rush = isLunchRush(new Date()) ? 1 : 0;
182 return Math.round((base + perKm + rush) * 100) / 100;
183}
184
185function newId() {
186 return "BB-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
187}
188
189function log(event, ...details) {
190 console.log("[" + new Date().toISOString() + "] " + event, ...details);
191}
192
193function notify(phone, text) {
194 if (!phone) return;
195 sms.send(phone, text.slice(0, 160));
196}
197
198function saveUser(user) {
199 db.users.put(user.id, user);
200}
201
202function loadUser(id) {
203 return db.users.get(id);
204}
205
206function saveOrder(order) {
207 db.orders.put(order.id, order);
208 log("saved", order.id);
209}
210
211function loadOrder(id) {
212 return db.orders.get(id);
213}
214
215function ordersToday(now) {
216 const day = now.toISOString().slice(0, 10);
217 return db.orders.all().filter((o) => o.placedAt.startsWith(day));
218}
219
220function revenueToday(now) {
221 return ordersToday(now).reduce((sum, o) => sum + o.total, 0);
222}
223
224function busiestHour(now) {
225 const counts = new Array(24).fill(0);
226 for (const o of ordersToday(now)) counts[new Date(o.placedAt).getHours()]++;
227 return counts.indexOf(Math.max(...counts));
228}
229
230// DEAD CODE: the CSV export the accountant asked for, then never used
231function exportCsv(orders) {
232 const rows = orders.map((o) => [o.id, o.total, o.status].join(","));
233 rows.unshift("id,total,status");
234 return rows.join("\n");
235}
236
237function ridersOnline(riders) {
238 return riders.filter((r) => r.online).length;
239}
240
241function riderReport(riders, now) {
242 const online = ridersOnline(riders);
243 const rush = isLunchRush(now);
244 return { online, rush, short: rush && online < 6 };
245}
246
247function weatherDelay(weather) {
248 if (weather === "rain") return 8;
249 if (weather === "snow") return 20;
250 return 0;
251}
252
253function adjustEta(order, weather) {
254 order.eta = order.eta + weatherDelay(weather);
255 return order;
256}
257
258function cancelOrder(order, reason) {
259 if (order.status === "delivered") return order;
260 order.status = "cancelled";
261 return refund(order, reason);
262}
263
264function reorder(previous, user, now) {
265 const cart = { items: previous.items.map((i) => ({ ...i })) };
266 return placeOrder(cart, user, now);
267}
268
269function favourite(user) {
270 const counts = {};
271 for (const o of db.orders.byUser(user.id)) {
272 for (const i of o.items) counts[i.id] = (counts[i.id] || 0) + i.qty;
273 }
274 return Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0];
275}
276
277module.exports = {
278 isOpen,
279 placeOrder,
280 assignRider,
281 markDelivered,
282 printTicket,
283 emailReceipt,
284 priceList,
285 revenueToday,
286 riderReport,
287 cancelOrder,
288 reorder,
289};
NORMALLoading the editor…1:1
Try it

Page by page

Targets further down (or back up) the file. The goal bar says the line; the strip shows where it is. Turn pages to get close, then a few lines to land.

1 of 6Move the cursor onto the
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.

Slide the page

Sometimes the line you're on is right, but it's stuck at the bottom of the screen. zz slides the page so your line sits in the middle, zt puts it at the top and zb at the bottom. The cursor stays on its line; only the page moves. Perfect when the team asks you to stop on a function and talk about it.

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

What moves?

9// legacy.js — Byte Burrito, 2019–present. Please don't add anything here.
8// (Everyone has added something here.)
7
6const TAX_RATE = 0.0875;
5const MAX_DISTANCE_KM = 8;
4const OPEN_HOUR = 11;
3const CLOSE_HOUR = 23;
2const RUSH_START = 12;
1const RUSH_END = 14;
10
1function isOpen(now) {
2 const hour = now.getHours();
NORMAL
Keys pressed: zz

The cursor is on line 10. You press zz. Which line is the cursor on afterwards?

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

Stop and talk

The team stops you on three functions. Being on the line isn't enough now: the page has to sit right too. zz puts your line in the middle, zt at the top so the function below it shows. The goal bar says which; the last one you have to reach 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.

From memory

Turning pages, from memory

Press the keys.

Half a page down.
scroll here, then press the keys
1 of 5
Half a page down.
Challenge

Reading the monolith

Twelve stops through legacy.js, getting further apart. Pages, half pages, page numbers: whatever reads best.

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

The code review

The review goes top to bottom, stopping on six functions. Get to each one; turning half pages is the way the team can follow, but anything works. Twice they'll want a whole function on screen: then its first line goes at the top too (the goal bar says when).

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

Keep reading

Endless stops through 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.