Skip to content

Commit 9582d3a

Browse files
committed
feat (day 134): implement travelling shopper with currency conversion and sequential affordability check
1 parent 0b3d268 commit 9582d3a

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""
2+
Traveling Shopper
3+
Given an amount of money you have, and an array of items you want to buy, determine how many of them you can afford.
4+
5+
The given amount will be in the format ["Amount", "Currency Code"]. For example: ["150.00", "USD"] or ["6000", "JPY"].
6+
Each array item you want to purchase will be in the same format.
7+
Use the following exchange rates to convert values:
8+
9+
Currency 1 Unit Equals
10+
USD 1.00 USD
11+
EUR 1.10 USD
12+
GBP 1.25 USD
13+
JPY 0.0070 USD
14+
CAD 0.75 USD
15+
If you can afford all the items in the list, return "Buy them all!".
16+
Otherwise, return "Buy the first X items.", where X is the number of items you can afford when purchased in the order given.
17+
18+
"""
19+
20+
import unittest
21+
22+
class TravelingShopperTest(unittest.TestCase):
23+
24+
def test1(self):
25+
self.assertEqual(buy_items(["150.00", "USD"], [["50.00", "USD"], ["75.00", "USD"], ["30.00", "USD"]]),"Buy the first 2 items.")
26+
27+
def test2(self):
28+
self.assertEqual(buy_items(["200.00", "EUR"], [["50.00", "USD"], ["50.00", "USD"]]),"Buy them all!")
29+
30+
def test3(self):
31+
self.assertEqual(buy_items(["100.00", "CAD"], [["20.00", "USD"], ["15.00", "EUR"], ["10.00", "GBP"], ["6000", "JPY"], ["5.00", "CAD"], ["10.00", "USD"]]),"Buy the first 3 items.")
32+
33+
def test4(self):
34+
self.assertEqual(buy_items(["5000", "JPY"], [["3.00", "USD"], ["1000", "JPY"], ["5.00", "CAD"], ["2.00", "EUR"], ["4.00", "USD"], ["2000", "JPY"]]),"Buy them all!")
35+
36+
def test5(self):
37+
self.assertEqual(buy_items(["200.00", "USD"], [["50.00", "USD"], ["40.00", "EUR"], ["30.00", "GBP"], ["5000", "JPY"], ["25.00", "CAD"], ["20.00", "USD"]]),"Buy the first 5 items.")
38+
39+
40+
41+
def buy_items(funds, items):
42+
43+
currency_table = {
44+
"USD": 1.00,
45+
"EUR": 1.10,
46+
"GBP": 1.25,
47+
"JPY": 0.0070,
48+
"CAD": 0.75
49+
}
50+
total_money, currency = funds
51+
currency_value = currency_table[currency]
52+
total_money_in_usd = float(total_money) * currency_value
53+
# total_money = float(total_money)
54+
55+
56+
57+
total = 0
58+
result_list = []
59+
60+
for price, curr in items:
61+
price = float(price) * currency_table[curr]
62+
result_list.append(price)
63+
total += price
64+
if total == total_money_in_usd:
65+
break
66+
elif total < total_money_in_usd:
67+
continue
68+
else:
69+
result_list.pop()
70+
break
71+
72+
# print(result_list)
73+
74+
if total == total_money_in_usd or total < total_money_in_usd:
75+
return "Buy them all!"
76+
else:
77+
return f"Buy the first {len(result_list)} items."
78+
79+
"""
80+
There are issues with the above code
81+
82+
1. Logic for "Buy them all!"
83+
-> The check:
84+
if total == total_money_in_usd or total < total_money_in_usd":
85+
return "Buy them all!"
86+
87+
-> But this condition is true even if you din't buy all items.
88+
Example. total_money_in_usd = 150 USD, items= [50, 60]
89+
-> After buying both, total= 110 and total_money_in_usd = 150
90+
-> Condition total < total_money_in_usd is true -> returns "Buy them all!" It is correct in some way though.
91+
1. The correct check should be: did we buy all items? not just whether total <= total_money_in_usd.
92+
93+
2. Result Tracking
94+
-> The above solution append every item cost to result_list, then pop if you overshoot.
95+
-> This works but is bit clunky. You can simply track a counter of how may items were bought.
96+
97+
3. Exact equality check
98+
-> Floating point comparisons
99+
total == total_money_in_usd => can be unreliable.
100+
-> Better to check if you managed to buy all items, regardless of leftover money.
101+
102+
103+
104+
The Logic flow of above solution
105+
106+
=> Accumulating total (spent so far).
107+
=> if total == wallet -> break (perfectly spent).
108+
=> if total < wallet -> continue (still have leftover)
109+
=> if total > wallet -> pop last item and break (ran out of money).
110+
111+
At the end:
112+
checking the condition
113+
if total == total_money_in_usd or total < total_money_in_usd:
114+
return "Buy them all!"
115+
else:
116+
return "Buy the first {len(result_list)} items."
117+
118+
119+
This can mislabel
120+
121+
Suppose:
122+
funds = ["150", "USD"]
123+
items = [["50", "USD"],["60", "USD"],["40", "USD"]]
124+
125+
-> Wallet = 150
126+
-> Buy 50 -> total = 50
127+
-> Buy 60 -> total = 110
128+
-> Try 40 -> total = 150 -> equal -> break
129+
-> Works fine here.
130+
131+
132+
But now:
133+
funds = ["150", "USD"]
134+
items = [["50", "USD"], ["60", "USD"]]
135+
136+
-> Wallet = 150
137+
-> Buy 50 -> total = 50
138+
-> Buy 60 -> total = 110
139+
-> End of list, total=110 < wallet
140+
-> The above condition says "Buy them all!" because total < wallet.
141+
-> That's corret in this case (you did buy all items, leftover is fine.)
142+
So far so good.
143+
144+
⚠️ Where it breaks
145+
The problem is the above condition doesn't actually check if all items were bought. It only checks
146+
if total <= wallet. That can be true even if you broke out early.
147+
148+
Example:
149+
150+
funds = ["100", "USD"]
151+
items = [["50", "USD"], ["60", "USD"], ["20", "USD"]]
152+
153+
-> Wallet = 100
154+
-> Buy 50 -> total = 50
155+
-> Try 60 -> total = 110 > wallet -> pop last item ,break
156+
-> End: total=50 < wallet
157+
158+
The above condition says "Buy them all!" because total < wallet
159+
160+
-> But you only bought 1 item, not all.
161+
162+
163+
"""
164+
"""
165+
Why this solution is safer
166+
167+
instead of comparing totals, this solution checks:
168+
169+
if count == len(items):
170+
return "Buy them all!"
171+
172+
That way, we only say "Buy them all!" if we actually iterated through and purchased
173+
every item in the list. Leftover money doesn't matter - you can still have leftover and it's fine.
174+
175+
Key Takeaway.
176+
177+
-> The previous solution works when you naturally reach the end of the list, but it can misfire if you break early due to overspending.
178+
-> The safer check is: did we buy all items? not just is total <= wallet
179+
-> Leftover money is perfectly fine - the difference is whether you stopped because you ran out or because
180+
you finished the list.
181+
182+
183+
This version
184+
-> Always normalize to USD using the exchange rates.
185+
-> Deduct item cost sequentially.
186+
-> Stop when funds run out.
187+
-> Return either "Buy them all!" or "Buy the first X items.".
188+
"""
189+
# This is the corrected version
190+
191+
def buy_items(funds, items):
192+
193+
currency_table = {
194+
"USD": 1.00,
195+
"EUR": 1.10,
196+
"GBP": 1.25,
197+
"JPY": 0.0070,
198+
"CAD": 0.75
199+
}
200+
201+
total_money, currency = funds
202+
total_money_in_usd = float(total_money) * currency_table[currency]
203+
204+
count = 0
205+
for price, curr in items:
206+
cost = float(price) * currency_table[curr]
207+
if total_money_in_usd >= cost:
208+
total_money_in_usd -= cost
209+
count += 1
210+
else:
211+
break
212+
if count == len(items):
213+
return "Buy them all!"
214+
else:
215+
return f"Buy the first {count} items."
216+
217+
218+
if __name__ == "__main__":
219+
print(buy_items(["150.00", "USD"], [["50.00", "USD"], ["75.00", "USD"], ["30.00", "USD"]]))
220+
print(buy_items(["200.00", "EUR"], [["50.00", "USD"], ["50.00", "USD"]]))
221+
222+
print(buy_items(["100.00", "CAD"], [["20.00", "USD"], ["15.00", "EUR"], ["10.00", "GBP"], ["6000", "JPY"], ["5.00", "CAD"], ["10.00", "USD"]]))
223+
unittest.main()

0 commit comments

Comments
 (0)