diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..dafd670c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "python.testing.pytestArgs": [ + "Work", + "--rootdir=Work", + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.formatting.provider": "none" +} \ No newline at end of file diff --git a/Notes/03_Program_organization/02_More_functions.md b/Notes/03_Program_organization/02_More_functions.md index 579a4b980..2c47872b7 100644 --- a/Notes/03_Program_organization/02_More_functions.md +++ b/Notes/03_Program_organization/02_More_functions.md @@ -501,7 +501,7 @@ For example: ```python >>> portfolio = parse_csv('Data/portfolio.dat', types=[str, int, float], delimiter=' ') >>> portfolio -[{'price': '32.20', 'name': 'AA', 'shares': '100'}, {'price': '91.10', 'name': 'IBM', 'shares': '50'}, {'price': '83.44', 'name': 'CAT', 'shares': '150'}, {'price': '51.23', 'name': 'MSFT', 'shares': '200'}, {'price': '40.37', 'name': 'GE', 'shares': '95'}, {'price': '65.10', 'name': 'MSFT', 'shares': '50'}, {'price': '70.44', 'name': 'IBM', 'shares': '100'}] +[{'name': 'AA', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'shares': 50, 'price': 91.1}, {'name': 'CAT', 'shares': 150, 'price': 83.44}, {'name': 'MSFT', 'shares': 200, 'price': 51.23}, {'name': 'GE', 'shares': 95, 'price': 40.37}, {'name': 'MSFT', 'shares': 50, 'price': 65.1}, {'name': 'IBM', 'shares': 100, 'price': 70.44}] >>> ``` diff --git a/Work/Data/stocksim.py b/Work/Data/stocksim.py index 52ebe4185..5ef258b56 100755 --- a/Work/Data/stocksim.py +++ b/Work/Data/stocksim.py @@ -112,13 +112,23 @@ def update(self): def incr(self,dt): self.time += dt if self.index < (len(self.history) - 2): - while self.index < (len(self.history) - 2) and self.time >= self.history[self.index+1][3]: + while self.index < (len(self.history) - 2) and \ + self.time >= self.history[self.index+1][3]: self.index += 1 self.update() def make_record(self): - return [self.name,round(self.price,2),self.date,minutes_to_str(self.time),round(self.change,2),self.open,round(self.high,2), - round(self.low,2),self.volume] + return [ + self.name, + round(self.price, 2), + self.date, + minutes_to_str(self.time), + round(self.change, 2), + self.open, + round(self.high, 2), + round(self.low, 2), + self.volume + ] class MarketSimulator(object): def __init__(self): diff --git a/Work/bounce.py b/Work/bounce.py index 3660ddd82..739f61c9d 100644 --- a/Work/bounce.py +++ b/Work/bounce.py @@ -1,3 +1,13 @@ # bounce.py # # Exercise 1.5 + +def bounce(): + current_height = 100 # meters + fall_fraction = 3 / 5 # fraction of from_height + + for i in range(1, 11): + current_height *= fall_fraction + print(i, round(current_height, 4)) + +bounce() \ No newline at end of file diff --git a/Work/bounce_test.py b/Work/bounce_test.py new file mode 100644 index 000000000..5d3569863 --- /dev/null +++ b/Work/bounce_test.py @@ -0,0 +1,17 @@ +from bounce import bounce + +def test_main(capfd): + bounce() + + out, err = capfd.readouterr() + assert out == """1 60.0 +2 36.0 +3 21.6 +4 12.96 +5 7.776 +6 4.6656 +7 2.7994 +8 1.6796 +9 1.0078 +10 0.6047 +""" diff --git a/Work/currency.py b/Work/currency.py new file mode 100644 index 000000000..6ffee15ab --- /dev/null +++ b/Work/currency.py @@ -0,0 +1,4 @@ +def usd(value: float) -> str: + """Format a number as USD.""" + neg = '-' if value < 0 else '' + return f"{neg}${abs(value):,.2f}" \ No newline at end of file diff --git a/Work/currency_test.py b/Work/currency_test.py new file mode 100644 index 000000000..98139a348 --- /dev/null +++ b/Work/currency_test.py @@ -0,0 +1,13 @@ +import pytest +from currency import usd + +@pytest.mark.parametrize("input,output", [ + (1, "$1.00"), + (11.5, "$11.50"), + (1.75, "$1.75"), + (1200.751, "$1,200.75"), + (-1245.758, "-$1,245.76"), + (-1.754, "-$1.75") +]) +def test_usd(input, output): + assert usd(input) == output \ No newline at end of file diff --git a/Work/dowstocks.py b/Work/dowstocks.py new file mode 100644 index 000000000..86c76cd47 --- /dev/null +++ b/Work/dowstocks.py @@ -0,0 +1,20 @@ +import csv + +def date_tuple(s): + return tuple(str(s).split('/')) + +def main(): + with open('Data/dowstocks.csv') as f: + rows = csv.reader(f) + headers = next(rows) + print(headers) + types = [str, float, date_tuple, str, float, float, float, float, int] + dow_stocks = [ + { key: func(val) for key, func, val in zip(headers, types, row) } + for row in rows + ] + print(dow_stocks[0]['date']) + print(len(dow_stocks)) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/Work/fileparse.py b/Work/fileparse.py index 1d499e733..0e35f5b00 100644 --- a/Work/fileparse.py +++ b/Work/fileparse.py @@ -1,3 +1,32 @@ # fileparse.py # # Exercise 3.3 +import csv + + +def parse_csv( + filename: str, + select: list = [], + types: list = [], + has_headers: bool = True, + delimiter: str = ",", +) -> list: + """ + Parse a CSV file into a list of records + """ + with open(filename) as f: + rows = csv.reader(f, delimiter=delimiter) + if has_headers: + orig_headers = next(rows) + headers = select if select else orig_headers + indices = [headers.index(name) for name in headers] + records = [] + for orig_row in rows: + if not orig_row: + continue + row = [orig_row[i] for i in indices] if has_headers else orig_row + if types: + row = [func(field) for func, field in zip(types, row)] + record = dict(zip(headers, row)) if has_headers else tuple(row) + records.append(record) + return records diff --git a/Work/fileparse_test.py b/Work/fileparse_test.py new file mode 100644 index 000000000..0fe4e8a0d --- /dev/null +++ b/Work/fileparse_test.py @@ -0,0 +1,84 @@ +from fileparse import parse_csv + + +def test_parse_csv_select_3_4(): + assert parse_csv("Data/portfolio.csv", select=["name", "shares"]) == [ + {"name": "AA", "shares": "100"}, + {"name": "IBM", "shares": "50"}, + {"name": "CAT", "shares": "150"}, + {"name": "MSFT", "shares": "200"}, + {"name": "GE", "shares": "95"}, + {"name": "MSFT", "shares": "50"}, + {"name": "IBM", "shares": "100"}, + ] + + +def test_parse_csv_select_3_5(): + assert parse_csv("Data/portfolio.csv", types=[str, int, float]) == [ + {"price": 32.2, "name": "AA", "shares": 100}, + {"price": 91.1, "name": "IBM", "shares": 50}, + {"price": 83.44, "name": "CAT", "shares": 150}, + {"price": 51.23, "name": "MSFT", "shares": 200}, + {"price": 40.37, "name": "GE", "shares": 95}, + {"price": 65.1, "name": "MSFT", "shares": 50}, + {"price": 70.44, "name": "IBM", "shares": 100}, + ] + + assert parse_csv( + "Data/portfolio.csv", select=["name", "shares"], types=[str, int] + ) == [ + {"name": "AA", "shares": 100}, + {"name": "IBM", "shares": 50}, + {"name": "CAT", "shares": 150}, + {"name": "MSFT", "shares": 200}, + {"name": "GE", "shares": 95}, + {"name": "MSFT", "shares": 50}, + {"name": "IBM", "shares": 100}, + ] + + +def test_parse_csv_no_headers_3_6(): + assert parse_csv("Data/prices.csv", types=[str, float], has_headers=False) == [ + ("AA", 9.22), + ("AXP", 24.85), + ("BA", 44.85), + ("BAC", 11.27), + ("C", 3.72), + ("CAT", 35.46), + ("CVX", 66.67), + ("DD", 28.47), + ("DIS", 24.22), + ("GE", 13.48), + ("GM", 0.75), + ("HD", 23.16), + ("HPQ", 34.35), + ("IBM", 106.28), + ("INTC", 15.72), + ("JNJ", 55.16), + ("JPM", 36.9), + ("KFT", 26.11), + ("KO", 49.16), + ("MCD", 58.99), + ("MMM", 57.1), + ("MRK", 27.58), + ("MSFT", 20.89), + ("PFE", 15.19), + ("PG", 51.94), + ("T", 24.79), + ("UTX", 52.61), + ("VZ", 29.26), + ("WMT", 49.74), + ("XOM", 69.35), + ] + + +def test_parse_csv_delimiter_3_7(): + assert parse_csv("Data/portfolio.dat", types=[str, int, float], delimiter=" ") == [ + { "name": "AA", "shares": 100, "price": 32.2 }, + { "name": "IBM", "shares": 50, "price": 91.1 }, + { "name": "CAT", "shares": 150, "price": 83.44 }, + { "name": "MSFT", "shares": 200, "price": 51.23 }, + { "name": "GE", "shares": 95, "price": 40.37 }, + { "name": "MSFT", "shares": 50, "price": 65.1 }, + { "name": "IBM", "shares": 100, "price": 70.44 }, + ] diff --git a/Work/hello.py b/Work/hello.py new file mode 100644 index 000000000..6b561325f --- /dev/null +++ b/Work/hello.py @@ -0,0 +1 @@ +print('Suh, Dude?') \ No newline at end of file diff --git a/Work/mortgage.py b/Work/mortgage.py index d527314e3..b79395b68 100644 --- a/Work/mortgage.py +++ b/Work/mortgage.py @@ -1,3 +1,37 @@ # mortgage.py # # Exercise 1.7 + +def mortgage(extra_payment_start_month=0, + extra_payment_end_month=0, + extra_payment=0.0): + principal = 500000.0 + rate = 0.05 + default_payment = 2684.11 + paid = 0.0 + month = 0 + + print(f'{"Mo":>3} {"Paid":>11} {"Principal":>11}') + while principal > 0: + month += 1 + payment = default_payment + + if month >= extra_payment_start_month and month <= extra_payment_end_month: + payment += extra_payment + + if payment > principal: + payment = principal + principal = 0 + else: + principal = principal * (1 + rate / 12) - payment + + paid = paid + payment + print(f'{month:>3} {f"${paid:,.2f}":>11} {f"${principal:,.2f}":>11}') + + print('------------------') + print(f'Total paid: ${round(paid, 2):,.2f}') + print('Months: ', month) + print('------------------') + return round(paid, 2) + +mortgage(61, 108, 1000) \ No newline at end of file diff --git a/Work/mortgage_test.py b/Work/mortgage_test.py new file mode 100644 index 000000000..75dbcefef --- /dev/null +++ b/Work/mortgage_test.py @@ -0,0 +1,7 @@ +from mortgage import mortgage + +def test_mortgage_1_7(): + assert round(mortgage(), 1) == 966266.9 + +def test_mortgage_1_8(): + assert round(mortgage(1, 12, 1000), 2) == 927989.46 \ No newline at end of file diff --git a/Work/pcost.py b/Work/pcost.py index e68aa20b4..d9f5dfb04 100644 --- a/Work/pcost.py +++ b/Work/pcost.py @@ -1,3 +1,21 @@ # pcost.py # # Exercise 1.27 +import csv + +def pcost(filename): + '''Returns the total cost of the portfolio''' + with open(filename, 'rt') as f: + lines = csv.reader(f) + cost = 0.0 + header_line = next(lines) + print(header_line) + for line_no, line in enumerate(lines): + record = dict(zip(header_line, line)) + try: + shares = int(record['shares']) + price = float(record['price']) + cost += shares * price + except ValueError: + print(f'Could not parse line {line_no} with data: {line}') + return cost \ No newline at end of file diff --git a/Work/pcost_command.py b/Work/pcost_command.py new file mode 100644 index 000000000..4f7d1d531 --- /dev/null +++ b/Work/pcost_command.py @@ -0,0 +1,15 @@ +# Excersize 1.33 + +from pcost import pcost +import sys + +print(sys.argv) +print() + +if len(sys.argv) > 1: + filename = sys.argv[1] +else: + filename = 'Data/portfolio.csv' + +cost = pcost(filename) +print(f'Total cost ${cost:,.2f}') \ No newline at end of file diff --git a/Work/pcost_test.py b/Work/pcost_test.py new file mode 100644 index 000000000..e9effb416 --- /dev/null +++ b/Work/pcost_test.py @@ -0,0 +1,13 @@ +from pcost import pcost +from os import path + +data_dir = path.join(path.dirname(__file__), 'Data') + +def test_pcost_1_27(): + assert pcost(path.join(data_dir, 'portfolio.csv')) == 44671.15 + +def test_pcost_1_31(): + assert pcost(path.join(data_dir, 'missing.csv')) == 27381.15 + +def test_pcost_2_16(): + assert pcost(path.join(data_dir, 'portfoliodate.csv')) == 44671.15 \ No newline at end of file diff --git a/Work/report.py b/Work/report.py index 47d5da7b1..d7c98b78a 100644 --- a/Work/report.py +++ b/Work/report.py @@ -1,3 +1,90 @@ # report.py # # Exercise 2.4 +import csv +from currency import usd + +def read_portfolio_2_4(filename): + portfolio = [] + with open(filename, 'rt') as f: + rows = csv.reader(f) + next(rows) # skip header + for fields in rows: + try: + holding = (fields[0], int(fields[1]), float(fields[2])) + portfolio.append(holding) + except ValueError: + print(f'Could not parse {fields}') + return portfolio + +def read_portfolio_2_5(filename): + portfolio = [] + with open(filename, 'rt') as f: + rows = csv.reader(f) + header = next(rows) # skip header + for row_no, fields in enumerate(rows): + try: + record = dict(zip(header, fields)) + holding = { + 'name': record['name'], + 'shares': int(record['shares']), + 'price': float(record['price']) + } + portfolio.append(holding) + except ValueError: + print(f'Could not parse row {row_no} with data {fields}') + return portfolio + +def read_prices_2_6(filename): + prices = {} + with open(filename, 'rt') as f: + rows = csv.reader(f) + # next(rows) # skip header + for row in rows: + if (len(row) == 0): + continue + + try: + prices[row[0]] = float(row[1]) + except ValueError: + print(f'Could not parse {row}') + + # pprint(prices) + return prices + +def get_gainloss_2_7(stocksFilename, pricesFilename): + prices = read_prices_2_6(pricesFilename) + stocks = read_portfolio_2_5(stocksFilename) + + total_value = 0.0 + total_market_value = 0.0 + total_gain = 0.0 + + for stock in stocks: + market_price = prices[stock['name']] + stock['change'] = market_price - stock['price'] + stock['current_value'] = stock['price'] * stock['shares'] + stock['market_value'] = market_price * stock['shares'] + stock['value_gain'] = stock['market_value'] - stock['current_value'] + + total_value += stock['current_value'] + total_market_value += stock['market_value'] + total_gain += stock['value_gain'] + + return (total_gain, stocks) + +def make_report_2_9( + stocksFilename, + pricesFilename): + + (_, stocks) = get_gainloss_2_7( + stocksFilename, + pricesFilename) + + print() + print(' '.join([f'{h:>10s}' for h in ['Name', 'Shares', 'Price', 'Change']])) + print(' '.join(['----------' for _ in range(4)])) + + for stock in stocks: + (name, shares, price, change, _, _, _) = stock.values() + print(f'{name:>10s} {shares:>10d} {usd(price):>10s} {usd(change):>10s}') \ No newline at end of file diff --git a/Work/report_command.py b/Work/report_command.py new file mode 100644 index 000000000..8c7fae4ba --- /dev/null +++ b/Work/report_command.py @@ -0,0 +1,8 @@ +from os import path +from report import make_report_2_9 + +data_dir = path.join(path.dirname(__file__), 'Data') + +make_report_2_9( + path.join(data_dir, 'portfolio.csv'), + path.join(data_dir, 'prices.csv')) \ No newline at end of file diff --git a/Work/report_test.py b/Work/report_test.py new file mode 100644 index 000000000..968b4e8bd --- /dev/null +++ b/Work/report_test.py @@ -0,0 +1,47 @@ +from os import path +from report import read_portfolio_2_4, read_portfolio_2_5, read_prices_2_6, get_gainloss_2_7 + +data_dir = path.join(path.dirname(__file__), 'Data') + +def test_read_portfolio_2_4(): + portfolio = read_portfolio_2_4(path.join(data_dir, 'portfolio.csv')) + assert portfolio == [ + ('AA', 100, 32.2), + ('IBM', 50, 91.1), + ('CAT', 150, 83.44), + ('MSFT', 200, 51.23), + ('GE', 95, 40.37), + ('MSFT', 50, 65.1), + ('IBM', 100, 70.44) + ] + +def test_read_portfolio_2_5(): + portfolio = read_portfolio_2_5(path.join(data_dir, 'portfolio.csv')) + assert portfolio == [ + {'name': 'AA', 'shares': 100, 'price': 32.2}, + {'name': 'IBM', 'shares': 50, 'price': 91.1}, + {'name': 'CAT', 'shares': 150, 'price': 83.44}, + {'name': 'MSFT', 'shares': 200, 'price': 51.23}, + {'name': 'GE', 'shares': 95, 'price': 40.37}, + {'name': 'MSFT', 'shares': 50, 'price': 65.1}, + {'name': 'IBM', 'shares': 100, 'price': 70.44} + ] + +def test_read_prices_2_6(): + prices = read_prices_2_6(path.join(data_dir, 'prices.csv')) + assert prices['AA'] == 9.22 + assert prices['AXP'] == 24.85 + assert prices['IBM'] == 106.28 + assert prices['MSFT'] == 20.89 + +def test_get_gainloss_2_7(): + (gain_loss, _) = get_gainloss_2_7( + path.join(data_dir, 'portfolio.csv'), + path.join(data_dir, 'prices.csv')) + assert round(gain_loss, 2) == -15985.05 + +def test_report_2_16(): + (gain_loss, _) = get_gainloss_2_7( + path.join(data_dir, 'portfoliodate.csv'), + path.join(data_dir, 'prices.csv')) + assert round(gain_loss, 2) == -15985.05 \ No newline at end of file diff --git a/Work/requirements.txt b/Work/requirements.txt new file mode 100644 index 000000000..5dbd22163 --- /dev/null +++ b/Work/requirements.txt @@ -0,0 +1,3 @@ +pytest==7.4.0 +pytest-watch==4.2.0 +ruff==0.0.282 diff --git a/Work/sears.py b/Work/sears.py new file mode 100644 index 000000000..7f946c2ae --- /dev/null +++ b/Work/sears.py @@ -0,0 +1,26 @@ +# One morning, you go out and place a dollar bill +# on the sidewalk by the Sears tower in Chicago. +# Each day thereafter, you go out double the number +# of bills. How long does it take for the stack of +# bills to exceed the height of the tower? + +bill_thickness = 0.11 * 0.001 # meters (0.11 mm) +sears_height = 442 # height (meters) +num_bills = 1 +day = 1 + +def print_day(day, num_bills, height): + remaining_height = sears_height - height + print('{:<10}{:<21}{:<20}{:<30}' + .format( + f'day: {day}', + f'num_bills: {num_bills}', + f'height: {height}', + f'height_remaining: {round(remaining_height, 2)}')) + +while num_bills * bill_thickness < sears_height: + print_day(day, num_bills, num_bills * bill_thickness) + day += 1 + num_bills *= 2 + +print_day(day, num_bills, num_bills * bill_thickness) \ No newline at end of file diff --git a/Work/work.ps1 b/Work/work.ps1 new file mode 100644 index 000000000..d7a38c3d2 --- /dev/null +++ b/Work/work.ps1 @@ -0,0 +1,24 @@ +param([string]$Command) + +switch ($Command) { + "venv" { + & python -m venv .venv + & $PSScriptRoot\.venv\Scripts\Activate.ps1 + & pip install -r $PSScriptRoot\requirements.txt + } + "test" { + & pytest $PSScriptRoot -q + } + "test:watch" { + & ptw $PSScriptRoot -- -q + } + "lint" { + & ruff check . + } + "lint:fix" { + & ruff check . --fix + } + default { + Write-Error "Command '$Command' not found." + } +} \ No newline at end of file