Skip to content

Commit e87182f

Browse files
committed
Upgrade logic for GPUCreditTracker class
1 parent 931d7ce commit e87182f

3 files changed

Lines changed: 157 additions & 99 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ test-js:
1818

1919
test-py:
2020
@echo "Running all Python tests..."
21-
uv run pytest
21+
uv run pytest -s

src/miscellaneous-code-challenges/gpu-credit-tracker/gpu_credit_tracker.py

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
from heapq import heappop, heappush
44
from typing import List, Optional
55

6+
from enum import Enum
7+
8+
9+
class Action(Enum):
10+
GRANT = "grant"
11+
SUBTRACT = "subtract"
12+
613

714
@dataclass
815
class Transaction:
@@ -11,72 +18,64 @@ class Transaction:
1118
at a given point in time
1219
"""
1320

14-
action: str
15-
amount: int
21+
action: Action
1622
timestamp: int
17-
expiration_timestamp: Optional[int]
18-
19-
def __init__(
20-
self,
21-
action: str,
22-
timestamp: int,
23-
amount: int,
24-
expiration_timestamp: Optional[int],
25-
):
26-
self.action = action
27-
self.amount = amount
28-
self.timestamp = timestamp
29-
self.expiration_timestamp = expiration_timestamp
23+
amount: int
24+
expiration_timestamp: Optional[int] = field(default=None)
3025

3126
def __lt__(self, other_value) -> bool:
27+
"""Compares two transactions. Required for heap comparisons. Only works with grants."""
28+
if self.action != Action.GRANT:
29+
return False
30+
3231
return self.expiration_timestamp < other_value.expiration_timestamp
3332

3433

35-
@dataclass
3634
class GPUCreditTracker:
3735
"""
3836
Implements the ability to create GPU credit grants, subtract credits, and get the credit
3937
balance at a given point in time.
4038
"""
4139

42-
transactions: List[Transaction] = field(default_factory=list)
40+
def __init__(self):
41+
self.transactions: List[Transaction] = []
4342

4443
def create_grant(
4544
self, timestamp: int, expiration_timestamp: int, amount: int
4645
) -> None:
47-
"""Creates a grant for a credit balance increase"""
48-
transaction = Transaction("grant", timestamp, amount, expiration_timestamp)
46+
"""Creates a grant for a credit balance increase."""
47+
transaction = Transaction(Action.GRANT, timestamp, amount, expiration_timestamp)
4948
self.__insert_transaction(transaction)
5049

5150
def subtract(self, timestamp: int, amount: int) -> None:
52-
"""Subtracts credits from the current balance if possible"""
53-
transaction = Transaction("subtract", timestamp, amount, None)
51+
"""Subtracts credits from the current balance if possible."""
52+
transaction = Transaction(Action.SUBTRACT, timestamp, amount, None)
5453
self.__insert_transaction(transaction)
5554

56-
def get_balance(self, timestamp: int) -> int:
57-
"""Returns the balance at a certain point in time"""
58-
grant_heap = []
59-
current = 0
55+
def get_balance(self, timestamp: int) -> int | None:
56+
"""Returns the balance at a certain point in time."""
57+
has_gone_negative = False
58+
grant_heap: list[Transaction] = []
59+
i = 0
6060

6161
while (
62-
current < len(self.transactions)
63-
and self.transactions[current].timestamp <= timestamp
62+
i < len(self.transactions) and self.transactions[i].timestamp <= timestamp
6463
):
65-
current_transaction = deepcopy(self.transactions[current])
64+
current_transaction = deepcopy(self.transactions[i])
6665

6766
# Clear out stale grants that have expired and can no longer be used
6867
while (
69-
len(grant_heap) > 0
68+
grant_heap
7069
and grant_heap[0].expiration_timestamp < current_transaction.timestamp
7170
):
7271
heappop(grant_heap)
7372

74-
if current_transaction.action == "grant":
73+
if current_transaction.action == Action.GRANT:
7574
heappush(grant_heap, current_transaction)
76-
elif current_transaction.action == "subtract":
75+
elif current_transaction.action == Action.SUBTRACT:
7776
total_subtract_remaining = current_transaction.amount
7877

79-
while len(grant_heap) > 0 and total_subtract_remaining > 0:
78+
while grant_heap and total_subtract_remaining > 0:
8079
current_grant_remaining_amount = grant_heap[0].amount
8180

8281
if current_grant_remaining_amount >= total_subtract_remaining:
@@ -86,39 +85,49 @@ def get_balance(self, timestamp: int) -> int:
8685
total_subtract_remaining -= grant_heap[0].amount
8786
heappop(grant_heap)
8887

89-
current += 1
88+
# Any leftover balance at a timestamp means that the account is in a not recoverable
89+
# state and None should always be returned from this point forward. Exit immediately.
90+
if total_subtract_remaining > 0:
91+
has_gone_negative = True
92+
break
93+
94+
i += 1
9095

96+
# Calculate the remaining balance from the grant heap if not in unrecoverable state
9197
total = 0
92-
while len(grant_heap) > 0:
98+
while not has_gone_negative and grant_heap:
9399
current_grant = heappop(grant_heap)
94100

95101
if current_grant.expiration_timestamp > timestamp:
96102
total += current_grant.amount
97103

98-
return total if total > 0 else 0
104+
return total if not has_gone_negative else None
99105

100106
def __insert_transaction(self, transaction: Transaction) -> None:
101-
if len(self.transactions) == 0:
102-
self.transactions.append(transaction)
103-
else:
104-
current = 0
105-
106-
while current < len(self.transactions) and (
107-
(
108-
# Place grant transactions at beginning of same timestamp
109-
transaction.action == "grant"
110-
and self.transactions[current].timestamp < transaction.timestamp
111-
)
112-
or (
113-
# Place subtract transactions at end of same timestamp
114-
transaction.action == "subtract"
115-
and self.transactions[current].timestamp <= transaction.timestamp
107+
"""Inserts a new transaction into the list of transactions."""
108+
new_transaction: Transaction | None = transaction
109+
new_transactions_list: list[Transaction] = []
110+
111+
for i, t in enumerate(self.transactions):
112+
if i == 0 and new_transaction and new_transaction.timestamp < t.timestamp:
113+
new_transactions_list.insert(0, new_transaction)
114+
new_transaction = None
115+
116+
new_transactions_list.append(t)
117+
118+
if (
119+
i + 1 < len(self.transactions)
120+
and new_transaction
121+
and (
122+
t.timestamp
123+
< new_transaction.timestamp
124+
< self.transactions[i + 1].timestamp
116125
)
117126
):
118-
current += 1
127+
new_transactions_list.append(new_transaction)
128+
new_transaction = None
129+
130+
if new_transaction:
131+
new_transactions_list.append(new_transaction)
119132

120-
self.transactions = (
121-
self.transactions[:current]
122-
+ [transaction]
123-
+ self.transactions[current:]
124-
)
133+
self.transactions = new_transactions_list
Lines changed: 91 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,94 @@
1-
import unittest
2-
31
from gpu_credit_tracker import GPUCreditTracker
42

53

6-
class TestGPUCreditTracker(unittest.TestCase):
7-
def test_inserts_correctly(self):
8-
"""Tests if the class inserts transactions correctly and gets the balance"""
9-
tracker = GPUCreditTracker()
10-
tracker.create_grant(10, 50, 5)
11-
tracker.create_grant(20, 60, 5)
12-
self.assertEqual(tracker.get_balance(19), 5)
13-
self.assertEqual(tracker.get_balance(20), 10)
14-
self.assertEqual(tracker.get_balance(50), 5)
15-
self.assertEqual(tracker.get_balance(60), 0)
16-
17-
def test_subtracts_in_any_order_entered(self):
18-
"""Tests if subtraction can be handled in any order that it's received"""
19-
tracker = GPUCreditTracker()
20-
tracker.create_grant(10, 50, 5)
21-
tracker.create_grant(20, 60, 5)
22-
self.assertEqual(tracker.get_balance(19), 5)
23-
tracker.subtract(15, 4)
24-
self.assertEqual(tracker.get_balance(10), 5)
25-
self.assertEqual(tracker.get_balance(20), 6)
26-
tracker.create_grant(15, 20, 8)
27-
self.assertEqual(tracker.get_balance(14), 5)
28-
self.assertEqual(tracker.get_balance(15), 9)
29-
self.assertEqual(tracker.get_balance(20), 10)
30-
31-
def test_should_return_zero_for_no_grants(self):
32-
"""Returns 0 for get_balance() if no grants have been created"""
33-
tracker = GPUCreditTracker()
34-
self.assertEqual(tracker.get_balance(10), 0)
35-
36-
def test_handles_all_grants_expired(self):
37-
"""Returns 0 if all grants have been expired"""
38-
tracker = GPUCreditTracker()
39-
tracker.create_grant(10, 50, 5)
40-
tracker.create_grant(20, 60, 5)
41-
self.assertEqual(tracker.get_balance(100), 0)
42-
43-
44-
if __name__ == "__main__":
45-
unittest.main()
4+
def test_inserts_correctly():
5+
"""Tests if the class inserts transactions correctly and gets the balance"""
6+
credit = GPUCreditTracker()
7+
credit.create_grant(10, 50, 5)
8+
credit.create_grant(20, 60, 5)
9+
assert credit.get_balance(19) == 5
10+
assert credit.get_balance(20) == 10
11+
assert credit.get_balance(50) == 5
12+
assert credit.get_balance(60) == 0
13+
14+
15+
def test_subtracts_from_expiring_grants_first():
16+
"""Tests if subtraction can be handled from expiring grants first"""
17+
credit = GPUCreditTracker()
18+
credit.create_grant(10, 50, 5)
19+
credit.create_grant(14, 60, 10)
20+
assert credit.get_balance(15) == 15
21+
credit.subtract(15, 10)
22+
assert credit.get_balance(10) == 5
23+
assert credit.get_balance(20) == 5
24+
25+
26+
def test_subtracts_in_any_order_entered():
27+
"""Tests if subtraction can be handled in any order that it's received"""
28+
credit = GPUCreditTracker()
29+
credit.create_grant(10, 50, 5)
30+
credit.create_grant(20, 60, 5)
31+
assert credit.get_balance(19) == 5
32+
credit.subtract(15, 4)
33+
assert credit.get_balance(10) == 5
34+
assert credit.get_balance(20) == 6
35+
credit.create_grant(14, 20, 8)
36+
assert credit.get_balance(14) == 13
37+
assert credit.get_balance(15) == 9
38+
assert credit.get_balance(20) == 10
39+
40+
41+
def test_should_return_zero_for_no_grants():
42+
"""Returns 0 for get_balance() if no grants have been created"""
43+
credit = GPUCreditTracker()
44+
assert credit.get_balance(10) == 0
45+
46+
47+
def test_handles_all_grants_expired():
48+
"""Returns 0 if all grants have been expired"""
49+
credit = GPUCreditTracker()
50+
credit.create_grant(10, 50, 5)
51+
credit.create_grant(20, 60, 5)
52+
assert credit.get_balance(100) == 0
53+
54+
55+
def test_handles_negative_balance():
56+
"""Returns None if the balance goes negative"""
57+
credit = GPUCreditTracker()
58+
credit.create_grant(10, 50, 5)
59+
credit.subtract(15, 6)
60+
credit.create_grant(16, 20, 8)
61+
assert credit.get_balance(15) == None
62+
assert credit.get_balance(16) == None
63+
assert credit.get_balance(20) == None
64+
credit.create_grant(11, 100, 10)
65+
assert credit.get_balance(10) == 5
66+
assert credit.get_balance(11) == 15
67+
assert credit.get_balance(15) == 9
68+
assert credit.get_balance(16) == 17
69+
assert credit.get_balance(20) == 9
70+
71+
72+
def test_balance_before_first_transaction():
73+
"""Returns 0 for get_balance(ts) when ts is before the first transaction"""
74+
credit = GPUCreditTracker()
75+
credit.create_grant(10, 50, 5)
76+
assert credit.get_balance(5) == 0
77+
assert credit.get_balance(9) == 0
78+
79+
80+
def test_exact_expiration_boundary():
81+
"""Grant is not counted at the expiration instant (expiration is exclusive)"""
82+
credit = GPUCreditTracker()
83+
credit.create_grant(10, 50, 5)
84+
assert credit.get_balance(49) == 5
85+
assert credit.get_balance(50) == 0
86+
87+
88+
def test_insert_transaction_places_earlier_transaction_at_start():
89+
"""Transaction with timestamp before all existing ones is inserted at start, not end."""
90+
credit = GPUCreditTracker()
91+
credit.create_grant(20, 60, 5)
92+
credit.create_grant(10, 50, 3)
93+
assert credit.get_balance(15) == 3
94+
assert credit.get_balance(25) == 8

0 commit comments

Comments
 (0)