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

H M L

H, M and L jump to the top, middle and bottom of the page you can see (three lines in, with file beyond the edge); a count measures from the edge, 6H.

Scene

Friday, 11:00 a.m. Stand-up, at your screen again. "Go to that line near the top. No, the other one. The one in the middle." Everyone points; you don't have a mouse to point with.

You don't need one. Whatever is on screen is the open page, and the open page has a top, a middle and a bottom.

From memory

Warm-up from earlier chapters

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

A whole page back.
press the keys…
1 of 2
A whole page back.

The open page

H jumps to the top of the page you can see, M to its middle and L to its bottom: High, Middle, Low. They don't scroll anything; they move the cursor around what's already in view, so "the line in the middle" is one key.

A margin at the edges

This editor keeps three lines of margin between the cursor and the edge of the window (Vim calls it scrolloff). So when there's more file above the screen, H stops on the fourth line of the page, not the very top, and L on the fourth from the bottom. On the first page of a file there's nothing above, so H goes all the way to line 1.

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

Top, middle, bottom

Targets on the page you can see. H, M or L gets you close in one key; a few j or k finish it.

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.

Count from the edge

A count measures from the edge: 6H is the sixth line from the top of the page, 5L the fifth from the bottom. A count inside the margin stops at the margin: when there's file above, 2H lands where H does.

Try it

Same key, different page

Each goal opens the file at a new page first. Some targets are on that page, at its top or bottom; some are the first or last line of the whole file. H and L only know the page you're looking at; gg and G know the whole book. The goal bar says which you're after.

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

Where is the middle?

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

This whole 12-line file fits on screen, and the cursor is on line 2. You press M. Which line does it land on?

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

The open page, from memory

Press the keys.

To the top of the screen.
scroll here, then press the keys
1 of 4
To the top of the screen.
Challenge

Point at the screen

Twelve stops on the page in view. Top, middle, bottom, then a short step.

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

Stand-up

Five places the team points at on the first screen of legacy.js; the goal bar says what they said. One edge key each, and a step if you need one.

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

Pointing practice

Endless targets on the open page.

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.