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

The change list and gi

The book remembers where you wrote, too: g; goes back to the last change, g, forward, and gi picks up typing where you last stopped.

Scene

Wednesday, 2:50 a.m. Found it, and your /* lat? */ note was asking the right question: placeOrder hands distanceKm a whole address where it wants a position, so the distance isn't a number and the fee falls back to its flat rate. KITCHEN is passed whole too. You fix the first half, then dispatch messages about the lunch-rush fee and you change that too, then you read three other functions to be sure.

Now you want to get back to your fix. Not the pages you opened: the places you wrote.

From memory

Warm-up from earlier chapters

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

Turn back to the page before.
press the keys…
1 of 2
Turn back to the page before.

Where you wrote

Besides the pages you opened, the book remembers every place you wrote in it. g; goes back to the last change, g; again to the one before that, and g, comes forward again. No ribbons needed: every change leaves its own (changes close together on one line share one).

1aaa
1bbb
2ccc
3ddd
4eee
NORMALLoading the editor…1:1
Try it

Back to the fix

Three steps, top to bottom. Add .position after order.address in placeOrder, then make the rush fee 2 in deliveryFee with r. Then back to the first fix with g; g;, and give KITCHEN a .position too.

1 of 3Type .position where it's shown in green
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/30.0s1:1

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

3:05 a.m., a test orderFinish the exercise above to see it.
Check yourself

Which change?

1aaa
1bbb
2ccc
3ddd
4eee
NORMAL
Keys pressed: rXjjrYGg;

You change line 1, then line 3, then jump to the last line with G and press g;. Where are you?

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

Opened or wrote?

1function deliveryFee(order, km) {
2 const rush = isLunchRush(now) ? 1 : 0;
1}
2
3function etaMinutes(order, now) {
4 return order.eta;
5}
6
7function refund(order) {
8 return order;
9}
NORMAL
Keys pressed: r2/etaMinutes↵/refund↵Ctrl-o

You make the rush fee 2 with r2, then search for etaMinutes and then for refund to read them. Back to the fee: you press Ctrl-o. Where are you?

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

Pick up the pen

gi goes further: it puts you back in Insert mode exactly where you last stopped typing, wherever the cursor is now. Half-written comment, then you went to look at something: gi and carry on writing.

Try it

Finish the note

Start a note at the end of the rush line: // on call: check. Then jump away with G and come back to finish it with gi: the rush.

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

Where you wrote, from memory

Press the keys.

Back to the last place you changed something.
scroll here, then press the keys2 keys
1 of 3
Back to the last place you changed something.