Learn Practice Daily golf Reference
Navigation·Lesson 1 of 5·8 min·0/7 exercisesPro

Marks

Ribbons in the long book: ma leaves one, 'a opens the book at that line, `a puts you on the exact word, and '' or `` go back to where you were before the last jump.

Scene

Wednesday, 2:07 a.m. Your first week on call, and the pager wakes you: every order tonight paid the flat 1.50 delivery fee, however far the rider rode. The trail runs through legacy.js: distanceKm near the top, placeOrder and deliveryFee past the middle. You'll be going back and forth between them for the next hour.

In the long book you learned to turn to any page. Tonight you want to leave ribbons in a few.

From memory

Warm-up from earlier chapters

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

Add one to the number under the cursor.
press the keys…
1 of 2
Add one to the number under the cursor.

Leave a ribbon

ma leaves a ribbon named a at the cursor, the way a bookmark ribbon stays in a page. Wherever you go next, 'a opens the book at that ribbon again: the marked line, at its first non-blank. Any lowercase letter is a ribbon of its own, so mb leaves a second one.

Aftermak0mbG
1function placeOrder(cart, user, now) {
b2 const order = { id: newId(), items: cart.items, address: user.address, placedAt: now.toISOString() };
a3 const fee = deliveryFee(order, distanceKm(KITCHEN, order.address));
4 const problem = validateOrder(order);
5 if (problem) throw new Error(problem);
6 order.discount = studentDiscount(order, user);
7 order.total = totalFor(order) - order.discount + fee;
8 return order;
9}
10
11function deliveryFee(order, km) {
12 const base = km <= 2 ? 0 : 1.5;
Two ribbons: a on the call to distanceKm, b at the start of the line above. The cursor has moved on; the ribbons stay.
Try it

Ribbons left for you

While the pager rang you left two ribbons: a in deliveryFee, b in distanceKm. Each time, something pulls you to the far end of the book first. Open the book at the ribbon with 'a or 'b: two keys from anywhere, which no other route beats.

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

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

Try it

Your own ribbons

Now leave them yourself. Go to placeOrder and leave ribbon ma before you set off; go to distanceKm and leave mb. Then back and forth: placeOrder, distanceKm, placeOrder. Any route counts; with the ribbons, each trip back is two keys.

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.

The exact word

'a opens the page; the backtick, `a, also puts your finger back on the exact word you marked: same line, same column. Use it when you'll come back to change something in the middle of a line.

Check yourself

Page or word?

1function placeOrder(cart) {
2 const fee = deliveryFee(order, km);
1}
2
3function deliveryFee(order, km) {
4 return 1;
5}
NORMAL
Keys pressed: maG'a

The cursor is in order on line 2. You press ma, then G, then 'a. Where does the cursor land?

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

A note on the exact word

In placeOrder, put the cursor on order.address inside the call to distanceKm and leave ribbon ma there. Go and read distanceKm: it wants .lat and .lng. Then come back to the exact word with `a and leave yourself a note right in front of it: /* lat? */ .

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.

The ribbon Vim leaves for you

Every time you make a big jump (G, a search, 'a), Vim leaves a ribbon where you were. '' (two single quotes) goes back to that line; `` (two backticks) goes back to the exact spot, like `a. Press it again and you're back where you jumped to: the quickest way to look something up and return.

Try it

Look it up, come back

The cursor is on isLunchRush in deliveryFee. Look up what it does with a search for function isLunchRush, then come back to the exact word with ``.

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

Ribbons, from memory

Press the keys.

Leave ribbon c here.
scroll here, then press the keys2 keys
1 of 5
Leave ribbon c here.