diff --git a/.gitignore b/.gitignore index 496ee2c..af56f61 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +.idea/ diff --git a/assignments/assignment01.rb b/assignments/assignment01.rb index cf3a605..15f198a 100644 --- a/assignments/assignment01.rb +++ b/assignments/assignment01.rb @@ -37,13 +37,11 @@ def number_to_string(n, lang) # it accepts a string # and returns the same string with each word capitalized. +# assumes titleize does not need to preserve non-word characters in the original string, i.e., +# titleize "hEllo, WORLD" => "Hello World" def titleize(s) - words = s.split - caps = [] - words.each do |word| - caps << word.capitalize - end - caps.join " " + word_array = s.split(/\W+/) + (word_array.map { |w| w.capitalize }).join(" ") end # Your method should generate the following results: @@ -58,14 +56,16 @@ def titleize(s) # Write your own implementation of `reverse` called `my_reverse` # You may *not* use the built-in `reverse` method def my_reverse(s) - output = "" - letters = s.split "" - n = letters.length - while n > 0 - n -= 1 - output << letters[n] + reversed_array = [] + + string_array = s.split("") + while not string_array.empty? do + # take a character from the end of the original string and + # add it to the beginning of the reversed string + reversed_array.push string_array.pop end - output + + reversed_array.join("") end # Your method should generate the following results: @@ -80,8 +80,9 @@ def my_reverse(s) # Write a method `palindrome?` # that determines whether a string is a palindrome def palindrome?(s) - stripped = s.delete(" ").delete(",").downcase - stripped == stripped.reverse + # remove all non-word characters, lowercase the string + canonical_string = s.gsub(/\W+/, '').downcase + canonical_string == canonical_string.reverse end # Your method should generate the following results: diff --git a/assignments/assignment02-statement.html b/assignments/assignment02-statement.html new file mode 100644 index 0000000..c1c7dde --- /dev/null +++ b/assignments/assignment02-statement.html @@ -0,0 +1,537 @@ + + + + + + Monthly Bank Statement + + + +

Withdrawals

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatePayeeAmount
12/01/2014Walgreens$21.92
12/04/2014Subway$7.80
12/04/2014Starbucks$5.30
12/05/2014Nike Town$85.00
12/05/2014ATM$60.00
12/06/2014Starbucks$5.30
12/07/2014Safeway$23.89
12/07/2014Arco$42.00
12/07/2014check #5132$75.00
12/08/2014Starbucks$5.30
12/09/2014Bartell's$7.83
12/09/2014check #5128$575.00
12/11/2014Safeway$42.15
12/11/2014Home Depot$17.89
12/12/2014ATM$60.00
12/13/2014check #5134$45.68
12/13/2014check #5126$25.00
12/13/2014QFC$13.25
12/14/2014Office Depot$24.85
12/14/2014Chipotle$10.42
12/14/2014check #5133$17.50
12/15/2014Starbucks$5.30
12/15/2014ATM$60.00
12/16/20147-Eleven$5.26
12/16/2014check #5127$50.00
12/16/2014Arco$48.01
12/17/2014Apple$110.62
12/18/2014Starbucks$5.30
12/18/2014Bartell's$14.82
12/18/2014Starbucks$5.30
12/18/2014check #5129$125.00
12/19/2014Chipotle$10.42
12/22/2014check #5130$120.00
12/22/2014Shell$52.00
12/22/2014Safeway$28.45
12/22/2014QFC$48.13
12/23/2014Starbucks$5.30
12/26/2014check #5135$62.50
12/27/2014Starbucks$5.30
12/27/2014Walgreens$14.56
12/28/2014check #5136$62.50
12/29/2014Starbucks$5.30
12/30/2014ATM$100.00
12/30/2014Safeway$35.17
12/31/2014Starbucks$8.45
12/31/2014check #5131$20.00
+ + +

Deposits

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatePayeeAmount
12/04/2014Univ Washington$1500.00
12/06/2014ATM$50.00
12/06/2014ATM$500.00
12/15/2014ATM$50.00
12/17/2014Univ Washington$1500.00
12/21/2014ATM$20.00
+ + +

Daily Balance

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateAmount
12/01/2014$-21.92
12/02/2014$-21.92
12/03/2014$-21.92
12/04/2014$1464.98
12/05/2014$1319.98
12/06/2014$1864.68
12/07/2014$1723.79
12/08/2014$1718.49
12/09/2014$1135.66
12/10/2014$1135.66
12/11/2014$1075.62
12/12/2014$1015.62
12/13/2014$931.69
12/14/2014$878.92
12/15/2014$863.62
12/16/2014$760.35
12/17/2014$2149.73
12/18/2014$1999.31
12/19/2014$1988.89
12/20/2014$1988.89
12/21/2014$2008.89
12/22/2014$1760.31
12/23/2014$1755.01
12/24/2014$1755.01
12/25/2014$1755.01
12/26/2014$1692.51
12/27/2014$1672.65
12/28/2014$1610.15
12/29/2014$1604.85
12/30/2014$1469.68
12/31/2014$1441.23
+ + +

Summary

+ + + + + + + + + + + + + + + + + + + + + + +
Starting balance$0.00
Total deposits$3620.00
Total withdrawals$2178.77
Ending balance$1441.23
+ + + + + diff --git a/assignments/assignment02.rb b/assignments/assignment02.rb index 532f55c..7e68236 100644 --- a/assignments/assignment02.rb +++ b/assignments/assignment02.rb @@ -2,6 +2,9 @@ # Assignment 2 # ======================================================================================== +require 'bigdecimal' +require 'date' + # ======================================================================================== # Problem 1 - `to_sentence` @@ -10,14 +13,10 @@ # creates an english string from array def to_sentence(ary) - if ary.length == 0 - [] - elsif ary.length == 1 - ary[0] - else - last = ary.pop - "#{ary.join(", ")} and #{last}" - end + # your implementation here + return ary.join("") if ary.length < 2 + last_element = ary.pop + "#{ary.join(", ")} and #{last_element}" end # Your method should generate the following results: @@ -32,20 +31,21 @@ def to_sentence(ary) # implement methods "mean", "median" on Array of numbers def mean(ary) - sum = ary.reduce(0) {|x, acc| acc + x} - sum.to_f / ary.length + # your implementation here + ary.reduce(:+) / ary.length end def median(ary) - sorted_ary = ary.sort - len = sorted_ary.length - mid_index = len/2 - if len.odd? + # your implementation here + # the array has to be sorted for the algorithm to work + sorted_ary = ary.sort() + mid_index = sorted_ary.length/2 + if sorted_ary.length.odd? + # if the number of elements is odd, the median is just the number in the middle sorted_ary[mid_index] else - mid_lo = sorted_ary[mid_index] - mid_hi = sorted_ary[mid_index+1] - (mid_lo + mid_hi)/2.0 + # if the number of elements is even, the median is the average of the two numbers in the middle + (sorted_ary[mid_index] + sorted_ary[mid_index + 1])/2.0 end end @@ -62,7 +62,8 @@ def median(ary) # implement method `pluck` on array of hashes def pluck(ary, key) - ary.map {|item| item[key]} + # your implementation here + ary.map {|m| m[key]} end # Your method should generate the following results: @@ -90,206 +91,169 @@ def pluck(ary, key) # - daily balance # - summary: # - starting balance, total deposits, total withdrawals, ending balance -def bank_statement - - # ------------------------------------------------------------------------ - # formatting helpers: - def format_currency(amount) - prefix = if amount < 0 - amount = -amount - "-" - end - s = amount.to_s - if amount < 10 - cents = "0#{s}" - dollars = "0" - elsif amount < 100 - cents = s - dollars = "0" - else - cents = s[-2, 2] - dollars = s[0, s.length-2] - if dollars.length > 3 - lower = dollars[-3, 3] - upper = dollars[0, dollars.length-3] - dollars = "#{upper},#{lower}" +STARTING_BALANCE = BigDecimal.new("0") + +def generate_html_statement(csv_filename) + transactions = read_transactions(csv_filename) + withdrawals = transactions.select { |t| t[:type] == :withdrawal } + deposits = transactions.select { |t| t[:type] == :deposit } + daily_balances = compute_daily_balances(STARTING_BALANCE, transactions) + deposits_total = deposits.reduce(0) { |sum, t| sum += t[:amount] } + withdrawals_total = withdrawals.reduce(0) { |sum, t| sum += t[:amount] } + summary = [] + summary << {label: "Starting balance", amount: STARTING_BALANCE} + summary << {label: "Total deposits", amount: deposits_total} + summary << {label: "Total withdrawals", amount: withdrawals_total} + summary << {label: "Ending balance", amount: STARTING_BALANCE + deposits_total - withdrawals_total} + + html = render_html("Monthly Bank Statement", withdrawals, deposits, daily_balances, summary) + + File.open("assignment02-statement.html", "w") do |output| + output.print html + end +end + +# return an array of daily balance records, each record has two keys: :date and :amount. +def compute_daily_balances(starting_balance, transactions) + # create a hash of date => transaction array + transactions_by_date = transactions.group_by { |t| t[:date] } + # create a hash of date => total amount of transactions for a given day + daily_changes = transactions_by_date.reduce(Hash.new(BigDecimal.new("0"))) do |h, (k, v)| + h[k] = v.reduce(0) do |total, t| + type = t[:type] + amount = t[:amount] + if type == :deposit + total += amount + elsif type == :withdrawal + total -= amount end end - "$ #{prefix}#{dollars}.#{cents}" - end - - def format_date(date) - month_string = date[:month] < 10 ? "0#{date[:month]}" : date[:month].to_s - day_string = date[:day] < 10 ? "0#{date[:day]}" : date[:day].to_s - "#{month_string}/#{day_string}/#{date[:year]}" - end - - # ------------------------------------------------------------------------ - # rendering: - def render_html(statement) - <<-HTML - - - Bank Statement - - - #{render_body statement} - - HTML + h end - - def render_body(statement) - <<-BODY - -

Bank Statement

- #{render_summary statement[:summary]} - #{render_txs statement[:withdrawals], "Withdrawals"} - #{render_txs statement[:deposits], "Deposits"} - #{render_daily_balances statement[:dates], statement[:daily_balances]} - - BODY - end - - def render_summary(summary) - <<-SUMMARY -

Summary

- - - - - -
Starting Balance #{format_currency summary[:starting_balance]}
Total Deposits #{format_currency summary[:sum_deposits]}
Total Withdrawals#{format_currency summary[:sum_withdrawals]}
Ending Balance #{format_currency summary[:ending_balance]}
- SUMMARY - end - - def render_tx(tx) - <<-TX - - #{tx[:formatted_date]} - #{tx[:payee]} - #{format_currency tx[:amount]} - - TX - end - - def render_txs(txs, label) - <<-TXS -

#{label}

- - #{txs.map {|tx| render_tx tx}.join "\n"} -
- TXS - end - - def render_daily_balance(date, balance) - <<-TXS - - #{date} - #{format_currency balance[:summary][:ending_balance]} - - TXS - end - - def render_daily_balances(dates, balances) - <<-BALANCES -

Daily Balances

- - #{dates.map {|date| render_daily_balance date, balances[date]}.join "\n"} -
- BALANCES - end - - # ------------------------------------------------------------------------ - # read csv file, generate txs: - def read_txs - File.open("assignment02-input.csv") do |file| - lines = file.readlines - - keys = lines.shift.chomp.split(",").map {|key| key.to_sym} - - lines.map do |line| - tx = {} - line.chomp.split(",").each_with_index do |field, index| - key = keys[index] - tx[key] = field - end - tx - end.map do |tx| - tx[:amount] = (tx[:amount].to_f * 100).to_i - month, day, year = tx[:date].split "/" - date = {year: year.to_i, month: month.to_i, day: day.to_i} - tx[:date] = date - tx[:formatted_date] = format_date date - tx - end.sort {|a,b| a[:formatted_date] <=> b[:formatted_date]} - end + + daily_balances = {} + current_balance = starting_balance + (transactions_by_date.keys.min..transactions_by_date.keys.max).each do |d| + current_balance += daily_changes[d] + daily_balances[d] = current_balance end - - # ------------------------------------------------------------------------ - # calc totals for collection of txs: - def txs_totals(txs, starting_balance) - withdrawals = txs.select {|tx| tx[:type] == "withdrawal"}.sort {|a,b| a[:formatted_date] <=> b[:formatted_date]} - sum_withdrawals = withdrawals.reduce(0) {|acc, tx| acc += tx[:amount]} - - deposits = txs.select {|tx| tx[:type] == "deposit"}.sort {|a,b| a[:formatted_date] <=> b[:formatted_date]} - sum_deposits = deposits.reduce(0) {|acc, tx| acc += tx[:amount]} - - ending_balance = starting_balance + sum_deposits - sum_withdrawals - - { - summary: { - starting_balance: starting_balance, - sum_deposits: sum_deposits, - sum_withdrawals: sum_withdrawals, - ending_balance: ending_balance - }, - withdrawals: withdrawals, - deposits: deposits - } + + daily_balances.reduce([]) do |ary, (k, v)| + ary << {date: k, amount: v} end - - # ------------------------------------------------------------------------ - # calc statement from txs: - def calc_statement(txs) - statement = txs_totals txs, 0 - - dates = txs.map {|tx| tx[:formatted_date]}.uniq.sort - - daily_balances = {} - dates.each_with_index do |date, index| - txs_for_date = txs.select {|tx| tx[:formatted_date] == date} - starting_balance = if index == 0 - # first day, so use starting balance: - statement[:summary][:starting_balance] +end + +# returns an array of transactions, each represented as a map in Ruby +# the transactions are sorted by date +def read_transactions(csv_filename) + transactions = [] + + File.open(csv_filename, "r") do |f| + keys_array = nil + + f.each_with_index(sep="\r") do |line, line_num| + values = line.chomp().split(",") + + if line_num == 0 + keys_array = values.map { |value| value.to_sym } else - # use ending balance of previous date: - prev_date = dates[index-1] - daily_balances[prev_date][:summary][:ending_balance] + # create a map as a record for each transaction + transaction = {} + + # convert string values to Ruby types as appropriate + values.each_with_index do |value, i| + key = keys_array[i] + case key + when :date + value = Date.strptime(value, "%m/%d/%Y") + when :amount + # use BigDecimal to represent currency values + value = BigDecimal.new(value) + when :type + value = value.to_sym + end + transaction[key] = value + end + + transactions << transaction end - daily_balances[date] = txs_totals txs_for_date, starting_balance end - statement[:dates] = dates - statement[:daily_balances] = daily_balances - statement end - - # ------------------------------------------------------------------------ - # write html to file: - def write_html(html) - File.open("assignment02-output.html", "w") do |file| - file.write html - end + + # return a sorted list of transactions by date + transactions.sort { |t1, t2| t1[:date] <=> t2[:date] } +end + +def render_html(title, withdrawals, deposits, daily_balances, summary) +< + + #{render_head title} + #{render_body withdrawals, deposits, daily_balances, summary} + +HTML +end + +def render_head(title) +< + + + #{title} + +HEAD +end + +def render_body(withdrawals, deposits, daily_balances, summary) +< + #{render_section "Withdrawals", [:date, :payee, :amount], withdrawals, true} + #{render_section "Deposits", [:date, :payee, :amount], deposits, true} + #{render_section "Daily Balance", [:date, :amount], daily_balances, true} + #{render_section "Summary", [:label, :amount], summary, false} + +BODY +end + +def render_section(title, headers, rows, render_header) +<
#{title} + #{render_table headers, rows, render_header} +SECTION +end + +def render_table(headers, rows, render_header) +< + #{render_header ? render_table_header(headers) : ""} + #{rows.map {|r| render_row r, headers}.join "\n"} +
+TABLE +end + +def render_table_header(headers) +<
+ #{headers.map {|h| "#{h.to_s.capitalize}"}.join "\n"} + +HEADER +end + +def render_row(row, headers) +< + #{headers.map {|h| "#{format_value(row[h], h)}"}.join "\n"} + +ROW +end + +def format_value(value, header) + case header + when :date + value.strftime("%m/%d/%Y") + when :amount + '$%.2f' % value + else + value end - - # ------------------------------------------------------------------------ - txs = read_txs - statement = calc_statement txs - html = render_html statement - write_html html - nil end diff --git a/assignments/assignment03-statement.html b/assignments/assignment03-statement.html new file mode 100644 index 0000000..361b171 --- /dev/null +++ b/assignments/assignment03-statement.html @@ -0,0 +1,525 @@ + + + + + + Monthly Bank Statement + + +

Summary

+ + + + + + + + + + + + + + + + + +
Starting balance$0.00
Total deposits$3620.00
Total withdrawals$2178.77
Ending balance$1441.23
+ +

Withdrawals

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatePayeeAmount
12/01/2014Walgreens$21.92
12/04/2014Subway$7.80
12/04/2014Starbucks$5.30
12/05/2014Nike Town$85.00
12/05/2014ATM$60.00
12/06/2014Starbucks$5.30
12/07/2014Safeway$23.89
12/07/2014Arco$42.00
12/07/2014check #5132$75.00
12/08/2014Starbucks$5.30
12/09/2014Bartell's$7.83
12/09/2014check #5128$575.00
12/11/2014Safeway$42.15
12/11/2014Home Depot$17.89
12/12/2014ATM$60.00
12/13/2014check #5134$45.68
12/13/2014check #5126$25.00
12/13/2014QFC$13.25
12/14/2014Office Depot$24.85
12/14/2014Chipotle$10.42
12/14/2014check #5133$17.50
12/15/2014Starbucks$5.30
12/15/2014ATM$60.00
12/16/20147-Eleven$5.26
12/16/2014check #5127$50.00
12/16/2014Arco$48.01
12/17/2014Apple$110.62
12/18/2014Starbucks$5.30
12/18/2014Bartell's$14.82
12/18/2014Starbucks$5.30
12/18/2014check #5129$125.00
12/19/2014Chipotle$10.42
12/22/2014check #5130$120.00
12/22/2014Shell$52.00
12/22/2014Safeway$28.45
12/22/2014QFC$48.13
12/23/2014Starbucks$5.30
12/26/2014check #5135$62.50
12/27/2014Starbucks$5.30
12/27/2014Walgreens$14.56
12/28/2014check #5136$62.50
12/29/2014Starbucks$5.30
12/30/2014ATM$100.00
12/30/2014Safeway$35.17
12/31/2014Starbucks$8.45
12/31/2014check #5131$20.00
+ +

Deposits

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatePayeeAmount
12/04/2014Univ Washington$1500.00
12/06/2014ATM$50.00
12/06/2014ATM$500.00
12/15/2014ATM$50.00
12/17/2014Univ Washington$1500.00
12/21/2014ATM$20.00
+ +

Daily Balance

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateAmount
12/01/2014$-21.92
12/02/2014$-21.92
12/03/2014$-21.92
12/04/2014$1464.98
12/05/2014$1319.98
12/06/2014$1864.68
12/07/2014$1723.79
12/08/2014$1718.49
12/09/2014$1135.66
12/10/2014$1135.66
12/11/2014$1075.62
12/12/2014$1015.62
12/13/2014$931.69
12/14/2014$878.92
12/15/2014$863.62
12/16/2014$760.35
12/17/2014$2149.73
12/18/2014$1999.31
12/19/2014$1988.89
12/20/2014$1988.89
12/21/2014$2008.89
12/22/2014$1760.31
12/23/2014$1755.01
12/24/2014$1755.01
12/25/2014$1755.01
12/26/2014$1692.51
12/27/2014$1672.65
12/28/2014$1610.15
12/29/2014$1604.85
12/30/2014$1469.68
12/31/2014$1441.23
+ + + + + diff --git a/assignments/assignment03.rb b/assignments/assignment03.rb index ee5cd93..2e65a4e 100644 --- a/assignments/assignment03.rb +++ b/assignments/assignment03.rb @@ -2,11 +2,26 @@ # Assignment 3 # ======================================================================================== +require 'bigdecimal' +require 'date' + # ======================================================================================== # Problem 1 - re-implement titleize, palindrome? # re-implement titleize and palindrome? as methods on String +class String + def titleize + word_array = self.split(/\W+/) + word_array.map { |w| w.capitalize }.join(" ") + end + + def palindrome? + canonical_string = self.gsub(/\W+/, '').downcase + canonical_string == canonical_string.reverse + end +end + "hEllo WORLD".titleize #=> "Hello World" "gooDbye CRUel wORLD".titleize #=> "Goodbye Cruel World" @@ -23,6 +38,33 @@ # re-implement mean, median, to_sentence as methods on Array +class Array + def mean + self.reduce(:+).to_f / self.length + end + + def median + # the array has to be sorted for the algorithm to work + sorted_ary = self.sort + mid_index = sorted_ary.length/2 + if sorted_ary.length.odd? + # if the number of elements is odd, the median is just the number in the middle + sorted_ary[mid_index] + else + # if the number of elements is even, the median is the average of the two numbers in the middle + (sorted_ary[mid_index] + sorted_ary[mid_index + 1])/2.0 + end + end + + def to_sentence + return self.join("") if self.length < 2 + # make a copy of the array since pop is destructive + array_copy = Array.new(self) + last_element = array_copy.pop + "#{array_copy.join(", ")} and #{last_element}" + end +end + # Your method should generate the following results: [1, 2, 3].mean #=> 2 [1, 1, 4].mean #=> 2 @@ -47,4 +89,281 @@ # - DepositTransaction # - WithdrawalTransaction +class BankAccount + attr_reader :transactions + + def initialize(csv_filename) + @transactions = [] + + File.open(csv_filename, "r") do |f| + keys_array = nil + + f.each_with_index do |line, line_num| + values = line.chomp().split(",") + + if line_num == 0 + keys_array = values.map { |value| value.to_sym } + else + # create a map as a record for each transaction + fields = {} + + # convert string values to Ruby types as appropriate + values.each_with_index do |value, i| + key = keys_array[i] + case key + when :date + value = Date.strptime(value, "%m/%d/%Y") + when :amount + # use BigDecimal to represent currency values + value = BigDecimal.new(value) + when :type + value = value.to_sym + end + fields[key] = value + end + + @transactions << Transaction.create(fields) + end + end + end + + # return a sorted list of transactions by date + @transactions.sort! { |t1, t2| t1.date <=> t2.date } + end + + def starting_balance + BigDecimal("0") + end + + def ending_balance + starting_balance + total_deposits - total_withdrawals + end + + def total_withdrawals + withdrawals.reduce(BigDecimal("0")) { |sum, t| sum += t.amount } + end + + def total_deposits + deposits.reduce(BigDecimal("0")) { |sum, t| sum += t.amount } + end + + def withdrawals + @transactions.select { |t| t.class == WithdrawalTransaction } + end + + def deposits + @transactions.select { |t| t.class == DepositTransaction } + end + + # return an array of daily balance records, each record has two keys: :date and :amount. + def daily_balances + # create a hash of date => transaction array + transactions_by_date = @transactions.group_by { |t| t.date } + # create a hash of date => total amount of transactions for a given day + daily_changes = transactions_by_date.reduce(Hash.new(BigDecimal.new("0"))) do |h, (k, v)| + h[k] = v.reduce(0) do |total, t| + total += t.signed_amount + end + h + end + daily_balances = {} + current_balance = starting_balance + (transactions_by_date.keys.min..transactions_by_date.keys.max).each do |d| + current_balance += daily_changes[d] + daily_balances[d] = current_balance + end + daily_balances.reduce([]) do |ary, (k, v)| + ary << {date: k, amount: v} + end + end +end + +class Transaction + attr_reader :date, :payee, :amount + + # factory method for creating a transaction from a hash + def self.create(fields) + date = fields[:date] + payee = fields[:payee] + amount = fields[:amount] + type = fields[:type] + if type == :deposit + DepositTransaction.new(date, payee, amount) + elsif type == :withdrawal + WithdrawalTransaction.new(date, payee, amount) + end + end + + def initialize(date, payee, amount) + @date = date + @payee = payee + @amount = amount.abs + end + + def signed_amount + @amount + end +end + +class DepositTransaction < Transaction + def initialize(date, payee, amount) + super(date, payee, amount) + end +end + +class WithdrawalTransaction < Transaction + def initialize(date, payee, amount) + super(date, payee, amount) + end + + def signed_amount + -@amount + end +end + # use blocks for your HTML rendering code + +def generate_html_statement(csv_filename) + account = BankAccount.new(csv_filename) + + # invoke the render functions in a DSL-style-like structure + + html = render_html("Monthly Bank Statement", account) do |title, account| + render_head(title) + + render_body(account) do |account| + render_section("Summary", account) do |account| + render_summary_table(account) + end + + render_section("Withdrawals", account.withdrawals) do |transactions| + render_transaction_table(transactions) do |transaction| + render_transaction_row(transaction) + end + end + + render_section("Deposits", account.deposits) do |transactions| + render_transaction_table(transactions) do |transaction| + render_transaction_row(transaction) + end + end + + render_section("Daily Balance", account.daily_balances) do |daily_balances| + render_daily_balance_table(daily_balances) do |balances| + render_daily_balance_row(balances) + end + end + end + end + + # output the html + + File.open("assignment03-statement.html", "w") do |output| + output.print html + end +end + +def render_html(title, account, &block) +< + +#{block.call(title, account)} + +HTML +end + +def render_head(title) +< + + + #{title} + +HEAD +end + +def render_body(account, &block) +< +#{block.call(account)} + +BODY +end + +def render_section(title, section_params, &block) +<
#{title} +#{block.call(section_params)} +SECTION +end + +def render_transaction_table(transactions, &block) +< + + + + + + #{transactions.map { |r| block.call(r) }.join("\n")} +
DatePayeeAmount
+TABLE +end + +def render_transaction_row(row) +< + #{format_date(row.date)} + #{row.payee} + #{format_amount(row.amount)} + +ROW +end + +def render_daily_balance_table(daily_balances, &block) +< + + + + + #{daily_balances.map { |r| block.call(r) }.join("\n")} +
DateAmount
+TABLE +end + +def render_daily_balance_row(row) +< + #{format_date(row[:date])} + #{format_amount(row[:amount])} + +ROW +end + +def render_summary_table(account) +< + + + + + + + + + + + + + + + + +
Starting balance#{format_amount(account.starting_balance)}
Total deposits#{format_amount(account.total_deposits)}
Total withdrawals#{format_amount(account.total_withdrawals)}
Ending balance#{format_amount(account.ending_balance)}
+TABLE +end + +def format_date(date) + date.strftime("%m/%d/%Y") +end + +def format_amount(amount) + "$%.2f" % amount +end diff --git a/assignments/assignment04.rb b/assignments/assignment04.rb index d2443a2..94e6382 100644 --- a/assignments/assignment04.rb +++ b/assignments/assignment04.rb @@ -12,7 +12,13 @@ # F[n] -> F[n-2] + F[n-1] def fib(n) - # your implementation here + def fib_helper(n, acc1, acc2) + return acc1 if n == 0 + + fib_helper(n - 1, acc2, acc1 + acc2) + end + + fib_helper(n, 1, 1) end # expected behavior: @@ -24,46 +30,83 @@ def fib(n) # ======================================================================================== -# Problem 2 - Queue +# Problem 3 - LinkedList -# implement a Queue class that does not use Array. +# implement a LinkedList class that does not use Array. -# expected behavior: -q = Queue.new -q.empty? #=> true -q.enqueue "first" -q.empty? #=> false -q.enqueue "second" -q.dequeue #=> "first" -q.dequeue #=> "second" -q.dequeue #=> nil +class LinkedList + class Node + attr_accessor :item, :link -class Queue - def initialize - # your implementation here - end - def enqueue(item) - # your implementation here + def initialize(item, link) + @item = item + @link = link + end end - def dequeue - # your implementation here + + def initialize + @head = @tail = nil + @count = 0 end + def empty? - # your implementation here - end - def peek - # your implementation here + @count == 0 end + def length - # your implementation here + @count end -end + def <<(item) + node = Node.new(item, nil) + if @head.nil? + @head = @tail = node + else + @tail.link = node + @tail = node + end + @count += 1 + self + end -# ======================================================================================== -# Problem 3 - LinkedList + def first + @head.nil? ? nil : @head.item + end -# implement a LinkedList class that does not use Array. + def last + @tail.nil? ? nil : @tail.item + end + + def each(&block) + node = @head + while not node.nil? + block.call node.item + node = node.link + end + end + + # removes the first node matching item from the linked list + def delete(item) + node = @head + node_prev = nil + while not node.nil? + if node.item == item + if node_prev.nil? + @head = node.link + @tail = nil if @head.nil? + else + node_prev.link = node.link + end + node.link = nil + @count -= 1 + return node.item + end + node_prev = node + node = node.link + end + nil + end +end # expected behavior: ll = LinkedList.new @@ -87,26 +130,47 @@ def length ll.length #=> 2 ll.each {|x| puts x} #=> prints out "first", "third" -class LinkedList + +# ======================================================================================== +# Problem 2 - Queue + +# implement a Queue class that does not use Array. + +class Queue def initialize - # your implementation here + @list = LinkedList.new end - def empty? - # your implementation here - end - def length - # your implementation here + + def enqueue(item) + @list << item + self end - def <<(item) - # your implementation here + + def dequeue + return nil if empty? + @list.delete(@list.first) end - def first - # your implementation here + + def empty? + @list.empty? end - def last - # your implementation here + + def peek + return nil if empty? + @list.first.item end - def each(&block) - # your implementation here + + def length + @list.length end end + +# expected behavior: +q = Queue.new +q.empty? #=> true +q.enqueue "first" +q.empty? #=> false +q.enqueue "second" +q.dequeue #=> "first" +q.dequeue #=> "second" +q.dequeue #=> nil diff --git a/assignments/final_project.rb b/assignments/final_project.rb index a621230..c8984ff 100644 --- a/assignments/final_project.rb +++ b/assignments/final_project.rb @@ -2,6 +2,9 @@ # Final Project: Game of Life # ======================================================================================== +require 'minitest/autorun' +require 'tk' + # The Game of Life is a simplified model of evolution and natural selection # invented by the mathematician James Conway. @@ -45,6 +48,10 @@ # You choose how you want to render the current state of the board. # ASCII? HTML? Something else? +# Rendering employs a rendering strategy which is determined at the time a game is created. +# There are two rendering strategy implementations: ConsoleRenderStrategy, which outputs the state +# of the game to the console as a sequence of ASCII characters, and TkRenderStrategy, which draws +# the state of the game to a Tk Canvas. # --------------------------------------------------------------------------------------- # Bonus: DSL @@ -53,23 +60,315 @@ # - Your render method can then be formatted as the DSL, so that you can round-trip # between the textual DSL representation and the running instance. +# The game DSL has the following representation: +# gol 2, rendering_strategy, " +# .* +# *. +# " +# +# The first argument is the size of the board. +# The second argument is a rendering strategy. +# The third argument is a Ruby multiline string. Each row represents a row of the board. '*' represents a live cell, +# whereas '.' represents a dead cell. # --------------------------------------------------------------------------------------- # Suggested Implementation module FinalProject - class GameOfLife + class Cell + NUM_NEIGHBORS_PER_CELL = 8 + + attr_accessor :row, :col, :alive + alias_method :alive?, :alive + + def initialize(row, col, board) + @row = row + @col = col + @board = board + @alive = false + end + + # returns all neighboring cells + def neighbors + cells = [] + + (@row-1..@row+1).each do |row| + (@col-1..@col+1).each do |col| + cells << @board.cell_at(row, col) + end + end + cells.delete self + + cells + end + + # returns all live neighboring cells + def live_neighbors + neighbors.find_all { |cell| cell.alive? } + end + end + + class Board + # creates a board of size size * size + # the top-left most cell is row 0, col(umn) 0 + # the bottom-right most cell is row size - 1, col size - 1 def initialize(size) - # randomly initialize the board + @size = size + @board = [] + (0...size).each do |row| + @board << [] + (0...size).each do |col| + @board[row] << Cell.new(row, col, self) + end + end + end + + def each(&block) + (0...@size).each do |row| + (0...@size).each do |col| + block.call @board[row][col] + end + end + end + + def cell_at(row, col) + # allow wrapping in either direction + row += @size if row < 0 + col += @size if col < 0 + + @board[row % @size][col % @size] + end + + def to_s + @board.map do |row| + row.map do |cell| + cell.alive? ? '*' : '.' + end.join('') + end.join("\n") + end + end + + class GameOfLife + def initialize(size, render_strategy, board = nil) + @render_strategy = render_strategy + if board.nil? + @board = Board.new(size) + # randomly initialize the board + @board.each { |cell| cell.alive = [true, false].sample } + else + @board = board + end end + def evolve # apply rules to each cell and generate new state + new_states = [] + @board.each do |cell| + num_live_neighbors = cell.live_neighbors.length + if cell.alive? + new_state = num_live_neighbors == 2 || num_live_neighbors == 3 + else + new_state = num_live_neighbors == 3 + end + new_states << new_state + end + + # update the board with new states + @board.each do |cell| + cell.alive = new_states.shift + end end + def render # render the current state of the board + # delegate to the render strategy + @render_strategy.render @board end + def run(num_generations) # evolve and render for num_generations + (1..num_generations).each do |i| + evolve + render + end + end + end +end + +# render strategies + +module FinalProject + class ConsoleRenderStrategy + def render(board) + puts board.to_s + end + end + + class TkRenderStrategy + CELL_SIZE = 20 + + def initialize(size) + @size = size + canvas_size = @size * CELL_SIZE + root = TkRoot.new + canvas = TkCanvas.new(root) do + place height: canvas_size , width: canvas_size + end + + @cells = [] + (0...@size).each do |row| + (0...@size).each do |col| + top = row * CELL_SIZE + 3 + left = col * CELL_SIZE + 3 + bottom = (row + 1) * CELL_SIZE - 3 + right = (col + 1) * CELL_SIZE - 3 + @cells << TkcOval.new(canvas, left, top, right, bottom, width: 0, fill: "white") + end + end + end + + def render(board) + i = 0 + board.each do |cell| + @cells[i][:fill] = cell.alive? ? "green" : "white" + i += 1 + end + end + end +end + +# game DSL + +module FinalProject + class GameOfLifeBuilder + def initialize(size, render_strategy) + @size = size + @render_strategy = render_strategy + @board = Board.new(size) + end + + def build(board_string) + rows = board_string.split + rows.each_with_index do |row_string, row| + row_string.chars.each_with_index do |col_string, col| + @board.cell_at(row, col).alive = true if col_string == '*' + @board.cell_at(row, col).alive = false if col_string == '.' + end + end + GameOfLife.new(@size, @render_strategy, @board) + end + end +end + +def gol(size, render_strategy, board_string) + builder = FinalProject::GameOfLifeBuilder.new(size, render_strategy) + builder.build(board_string) +end + +# unit tests + +module FinalProject + class CellTest < Minitest::Test + def setup + @board = Board.new(8) + end + + def test_neighbors + # each cell should have exactly 8 neighbors + @board.each { |cell| assert_equal Cell::NUM_NEIGHBORS_PER_CELL, cell.neighbors.count } + end + + def test_live_neighbors + # each cell should have no live neighbors initially + @board.each { |cell| assert_equal 0, cell.live_neighbors.count } + # now set the top-left most cell as alive + # all neighbors of the alive cell should have live neighbors + cell0 = @board.cell_at(0, 0) + cell0.alive = true + cell0_neighbors = cell0.neighbors + @board.each do |cell| + if cell0_neighbors.include? cell + assert_equal 1, cell.live_neighbors.count + else + assert_equal 0, cell.live_neighbors.count + end + end + end + end + + class BoardTest < Minitest::Test + def setup + @board = Board.new(2) + @cells = [@board.cell_at(0, 0), @board.cell_at(0, 1), @board.cell_at(1, 0), @board.cell_at(1, 1)] + end + + def test_cell_at_wrapping + assert_equal @board.cell_at(0, 0), @board.cell_at(0, 2) + assert_equal @board.cell_at(0, 0), @board.cell_at(2, 0) + assert_equal @board.cell_at(0, 1), @board.cell_at(0, -1) + assert_equal @board.cell_at(0, 1), @board.cell_at(2, 1) + assert_equal @board.cell_at(1, 0), @board.cell_at(1, 2) + assert_equal @board.cell_at(1, 0), @board.cell_at(-1, 0) + assert_equal @board.cell_at(1, 1), @board.cell_at(1, -1) + assert_equal @board.cell_at(1, 1), @board.cell_at(-1, 1) + end + + def test_each_order + i = 0 + @board.each do |cell| + assert_equal @cells[i], cell + i += 1 + end + assert @cells.count, i + end + + def test_to_s + assert_equal "..\n..", @board.to_s + @cells[0].alive = true + assert_equal "*.\n..", @board.to_s + @cells[1].alive = true + assert_equal "**\n..", @board.to_s + @cells[2].alive = true + assert_equal "**\n*.", @board.to_s + @cells[3].alive = true + assert_equal "**\n**", @board.to_s + end + end + + class GameDSLTest < Minitest::Test + class MyRenderStrategy + attr_reader :rendered_value + + def render(board) + @rendered_value = board.to_s + end + end + + def setup + @render_strategy = MyRenderStrategy.new + end + + def test_two_by_two_game_dsl + game = gol 2, @render_strategy, ' + .* + *. + ' + + game.render + assert_equal ".*\n*.", @render_strategy.rendered_value + end + + def test_five_by_five_game_dsl + game = gol 5, @render_strategy, ' + .***. + *...* + *...* + *...* + .***. + ' + + game.render + assert_equal ".***.\n*...*\n*...*\n*...*\n.***.", @render_strategy.rendered_value end end end diff --git a/assignments/gol.gif b/assignments/gol.gif new file mode 100644 index 0000000..33cbb38 Binary files /dev/null and b/assignments/gol.gif differ diff --git a/assignments/run_life.rb b/assignments/run_life.rb new file mode 100644 index 0000000..bf34aef --- /dev/null +++ b/assignments/run_life.rb @@ -0,0 +1,14 @@ +# script for running Game of Life in a Tk Canvas + +load 'final_project.rb' + +BOARD_SIZE = 20 +DELAY_IN_MILLIS = 100 + +s = FinalProject::TkRenderStrategy.new(BOARD_SIZE) +g = FinalProject::GameOfLife.new(BOARD_SIZE, s) + +my_timer = TkTimer.new(DELAY_IN_MILLIS, -1, proc {g.render; g.evolve}) +my_timer.start + +Tk.mainloop diff --git a/assignments/run_life_dsl.rb b/assignments/run_life_dsl.rb new file mode 100644 index 0000000..978b57c --- /dev/null +++ b/assignments/run_life_dsl.rb @@ -0,0 +1,20 @@ +# script for running Game of Life in a Tk Canvas, where the game is created from a DSL + +load 'final_project.rb' + +BOARD_SIZE = 10 +DELAY_IN_MILLIS = 2000 + +s = FinalProject::TkRenderStrategy.new(BOARD_SIZE) +g = gol BOARD_SIZE, s, ' +.***. +*...**** +*...* +*...**** +.***. +' + +my_timer = TkTimer.new(DELAY_IN_MILLIS, -1, proc {g.render; g.evolve}) +my_timer.start + +Tk.mainloop diff --git a/assignments/style.css b/assignments/style.css new file mode 100644 index 0000000..104a0b5 --- /dev/null +++ b/assignments/style.css @@ -0,0 +1,23 @@ +h1, h2, th, td { + font-family: Helvetica +} + +.date { + width: 30%; + text-align: left; +} + +.payee { + width: 60%; + text-align: left; +} + +.label { + font-weight: bold; + width: 50%; + text-align: left; +} + +.amount { + text-align: right; +}