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

The jump list

The book remembers every page you opened: big jumps go on a trail, Ctrl-o turns back along it and Ctrl-i forward again. Not to be confused with Insert mode's Ctrl-o.

Scene

Wednesday, 2:31 a.m. Twenty jumps between placeOrder, deliveryFee and distanceKm, and somewhere along the way you looked at validateOrder and totalFor too, without leaving ribbons. Now you want to go back over exactly the path you took.

You don't need to remember it. The book does.

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 exact spot you were on before the last jump.
press the keys…2 keys
1 of 2
Back to the exact spot you were on before the last jump.

Every page you opened

Each big jump (G, gg, a search, n, { and }, %, a ribbon with 'a) is a page you opened, and the book keeps the trail in order. Turning pages by hand (j, even 13j, or w and f along a line) doesn't count. Ctrl-o turns back to the page before; Ctrl-i turns forward again.

After/validate↵/placeOrder↵gg
1function totalFor(order) {3Ctrl-o
5function validateOrder(order) {2Ctrl-o
10function placeOrder(cart, user, now) {Ctrl-o
1function totalFor(order) {You are here
Two searches and a jump to the top: from here, Ctrl-o walks back to where each search started.
Try it

Two pages back

The pager walked you through three pages with searches: validateOrder, totalFor, then placeOrder, where you are now. Two pages back with Ctrl-o Ctrl-o. Then a slip of the finger: one Ctrl-o too many, and you're at the top of the file, when the page you wanted next was totalFor. Forward along the trail with Ctrl-i, a page at a time.

1 of 2Two pages back: validateOrder
155// legacy.js — Byte Burrito, 2019–present. Please don't add anything here.
154// (Everyone has added something here.)
153
152const TAX_RATE = 0.0875;
151const MAX_DISTANCE_KM = 8;
150const OPEN_HOUR = 11;
149const CLOSE_HOUR = 23;
148const RUSH_START = 12;
147const RUSH_END = 14;
146
145function isOpen(now) {
144 const hour = now.getHours();
143 return hour >= OPEN_HOUR && hour < CLOSE_HOUR;
142}
141
140function isLunchRush(now) {
139 const hour = now.getHours();
138 return hour >= RUSH_START && hour < RUSH_END;
137}
136
135function formatPrice(cents) {
134 const dollars = Math.floor(cents / 100);
133 const rest = String(cents % 100).padStart(2, "0");
132 return "$" + dollars + "." + rest;
131}
130
129function subtotal(items) {
128 let sum = 0;
127 for (const item of items) {
126 sum += item.price * item.qty;
125 }
124 return sum;
123}
122
121function taxFor(amount) {
120 return Math.round(amount * TAX_RATE);
119}
118
117function totalFor(order) {
116 const base = subtotal(order.items);
115 const tax = taxFor(base);
114 const tip = order.tip || 0;
113 return base + tax + tip;
112}
111
110function validateOrder(order) {
109 if (!order.items || order.items.length === 0) return "empty";
108 if (order.items.some((i) => i.qty <= 0)) return "bad quantity";
107 if (!order.address) return "no address";
106 if (!isOpen(new Date())) return "closed";
105 return null;
104}
103
102function applyCoupon(order, code) {
101 const coupon = COUPONS[code];
100 if (!coupon) return order;
99 if (coupon.expires < Date.now()) return order;
98 const off = Math.round(subtotal(order.items) * coupon.percent);
97 return { ...order, discount: off };
96}
95
94function studentDiscount(order, user) {
93 if (!user.isStudent) return 0;
92 return Math.round(subtotal(order.items) * 0.1);
91}
90
89function distanceKm(a, b) {
88 const dLat = (b.lat - a.lat) * 111;
87 const dLng = (b.lng - a.lng) * 111 * Math.cos((a.lat * Math.PI) / 180);
86 return Math.sqrt(dLat * dLat + dLng * dLng);
85}
84
83function nearestRider(riders, address) {
82 let best = null;
81 for (const rider of riders) {
80 if (!rider.online) continue;
79 const d = distanceKm(rider.position, address.position);
78 if (d > MAX_DISTANCE_KM) continue;
77 if (!best || d < best.distance) best = { rider, distance: d };
76 }
75 return best;
74}
73
72function assignRider(order, riders) {
71 const match = nearestRider(riders, order.address);
70 if (!match) return refund(order, "No rider nearby, sorry!");
69 order.riderId = match.rider.id;
68 order.eta = Math.ceil(match.distance * 4) + 10;
67 notify(match.rider.phone, "New order: " + order.id);
66 return order;
65}
64
63function etaMinutes(order, now) {
62 const rush = isLunchRush(now) ? 10 : 0;
61 return order.eta + rush;
60}
59
58function refund(order, reason) {
57 order.status = "refunded";
56 order.refundReason = reason;
55 payments.refund(order.paymentId, totalFor(order));
54 notify(order.customerPhone, "Refunded: " + reason);
53 return order;
52}
51
50function markDelivered(order, now) {
49 order.status = "delivered";
48 order.deliveredAt = now.toISOString();
47 const late = now - new Date(order.placedAt) > order.eta * 60000;
46 if (late) order.lateBy = Math.round((now - new Date(order.placedAt)) / 60000 - order.eta);
45 return order;
44}
43
42function kitchenTicket(order) {
41 const lines = order.items.map((i) => i.qty + " x " + i.name);
40 lines.unshift("ORDER " + order.id);
39 if (order.notes) lines.push("NOTE: " + order.notes);
38 return lines.join("\n");
37}
36
35function printTicket(order) {
34 const ticket = kitchenTicket(order);
33 printer.print(ticket);
32 log("printed", order.id);
31}
30
29function emailReceipt(order, user) {
28 const body = renderReceipt(order);
27 mailer.send(user.email, "Your Byte Burrito receipt", body);
26}
25
24function menuItem(id) {
23 const item = MENU.find((m) => m.id === id);
22 if (!item) throw new Error("not on the menu: " + id);
21 return item;
20}
19
18function isSpicy(id) {
17 return menuItem(id).spicy === true;
16}
15
14function happyHourPrice(item, now) {
13 const hour = now.getHours();
12 if (hour >= 17 && hour < 19) return Math.round(item.price * 0.8);
11 return item.price;
10}
9
8function priceList(now) {
7 return MENU.map((item) => ({
6 id: item.id,
5 name: item.name,
4 price: formatPrice(happyHourPrice(item, now)),
3 }));
2}
1
156function 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}
16
17function newId() {
18 return "BB-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
19}
20
21function log(event, ...details) {
22 console.log("[" + new Date().toISOString() + "] " + event, ...details);
23}
24
25function notify(phone, text) {
26 if (!phone) return;
27 sms.send(phone, text.slice(0, 160));
28}
29
30function saveUser(user) {
31 db.users.put(user.id, user);
32}
33
34function loadUser(id) {
35 return db.users.get(id);
36}
37
38function saveOrder(order) {
39 db.orders.put(order.id, order);
40 log("saved", order.id);
41}
42
43function loadOrder(id) {
44 return db.orders.get(id);
45}
46
47function ordersToday(now) {
48 const day = now.toISOString().slice(0, 10);
49 return db.orders.all().filter((o) => o.placedAt.startsWith(day));
50}
51
52function revenueToday(now) {
53 return ordersToday(now).reduce((sum, o) => sum + o.total, 0);
54}
55
56function busiestHour(now) {
57 const counts = new Array(24).fill(0);
58 for (const o of ordersToday(now)) counts[new Date(o.placedAt).getHours()]++;
59 return counts.indexOf(Math.max(...counts));
60}
61
62function ridersOnline(riders) {
63 return riders.filter((r) => r.online).length;
64}
65
66function riderReport(riders, now) {
67 const online = ridersOnline(riders);
68 const rush = isLunchRush(now);
69 return { online, rush, short: rush && online < 6 };
70}
71
72function weatherDelay(weather) {
73 if (weather === "rain") return 8;
74 if (weather === "snow") return 20;
75 return 0;
76}
77
78function adjustEta(order, weather) {
79 order.eta = order.eta + weatherDelay(weather);
80 return order;
81}
82
83function cancelOrder(order, reason) {
84 if (order.status === "delivered") return order;
85 order.status = "cancelled";
86 return refund(order, reason);
87}
88
89function reorder(previous, user, now) {
90 const cart = { items: previous.items.map((i) => ({ ...i })) };
91 return placeOrder(cart, user, now);
92}
93
94function favourite(user) {
95 const counts = {};
96 for (const o of db.orders.byUser(user.id)) {
97 for (const i of o.items) counts[i.id] = (counts[i.id] || 0) + i.qty;
98 }
99 return Object.keys(counts).sort((a, b) => counts[b] - counts[a])[0];
100}
101
102module.exports = {
103 isOpen,
104 placeOrder,
105 assignRider,
106 markDelivered,
107 printTicket,
108 emailReceipt,
109 priceList,
110 revenueToday,
111 riderReport,
112 cancelOrder,
113 reorder,
114};
NORMALLoading the editor…1/20.0s156:1

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

Check yourself

Which page?

1a
1b
2c
3d
4e
5f
NORMAL
Keys pressed: jjGCtrl-o

From line 1 you press j j, then G, then Ctrl-o. Which line are you on?

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

Retrace your steps

Your own trail this time. Open three pages with jumps, not by scrolling: validateOrder, then totalFor, then placeOrder. Then retrace it: back to totalFor, back to validateOrder, and forward to totalFor again. Any route counts; the trail makes each step one key.

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.

Same keys, other mode

You've met Ctrl-o before, in Insert mode: there it borrows the remote for one Normal-mode command and hands the keyboard back, still typing. In Normal mode it turns back a page. Same keys, two jobs, so glance at the mode at the bottom of the editor before you press it.

Check yourself

Which Ctrl-O?

2function refund(order, reason) {
1 order.status = "refunded";
3 notify(order.customerPhone, reason);
1}
NORMAL
Keys pressed: ACtrl-ogg

You're typing at the end of line 3 (A) and press Ctrl-o, then gg. What happens?

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

The trail, from memory

Press the keys.

Turn back to the page before.
scroll here, then press the keys
1 of 3
Turn back to the page before.