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/assignment08.rb b/assignments/assignment08.rb index 3d46269..336275f 100644 --- a/assignments/assignment08.rb +++ b/assignments/assignment08.rb @@ -2,6 +2,9 @@ # Assignment 8 # ======================================================================================== +require 'minitest/autorun' +require 'bigdecimal' + # ======================================================================================== # Problem 1 - Roman Numerals @@ -10,48 +13,151 @@ module Assignment08 class RomanNumeral + LETTER_VALUES = { + I: 1, + V: 5, + X: 10, + L: 50, + C: 100, + D: 500, + M: 1000 + } + def initialize(i) - # your implementation here + raise ArgumentError, 'Number must be positive and less than 4000.' unless i > 0 and i < 4000 + @value = i end def to_s - # your implementation here + letters = [] + + i = @value + while i >= 1000 + letters << 'M' + i -= 1000 + end + if i >= 900 + letters << 'C' + letters << 'M' + i -= 900 + end + if i >= 500 + letters << 'D' + i -= 500 + end + if i >= 400 + letters << 'C' + letters << 'D' + i -= 400 + end + while i >= 100 + letters << 'C' + i -= 100 + end + if i >= 90 + letters << 'X' + letters << 'C' + i -= 90 + end + if i >= 50 + letters << 'L' + i -= 50 + end + if i >= 40 + letters << 'X' + letters << 'L' + i -= 40 + end + while i >= 10 + letters << 'X' + i -= 10 + end + if i >= 9 + letters << 'I' + letters << 'X' + i -= 9 + end + if i >= 5 + letters << 'V' + i -= 5 + end + if i >= 4 + letters << 'I' + letters << 'V' + i -= 4 + end + while i >= 1 + letters << 'I' + i -= 1 + end + + letters.join end def to_i - # your implementation here + @value end # bonus: create from Roman Numeral - def self.from_string - # your implementation here - # returns a new instance + def self.from_string(s) + values = s.chars.map {|letter| LETTER_VALUES[letter.to_sym]} + values.each_with_index do |value, i| + next if i == 0 + if value > values[i - 1] + values[i - 1] = -values[i - 1] + end + end + + RomanNumeral.new(values.reduce(:+)) + end + end + + class RomanNumeralTest < Minitest::Test + def test_to_s + assert_equal('I', RomanNumeral.new(1).to_s) + assert_equal('II', RomanNumeral.new(2).to_s) + assert_equal('III', RomanNumeral.new(3).to_s) + assert_equal('IV', RomanNumeral.new(4).to_s) + assert_equal('V', RomanNumeral.new(5).to_s) + assert_equal('VI', RomanNumeral.new(6).to_s) + assert_equal('IX', RomanNumeral.new(9).to_s) + assert_equal('X', RomanNumeral.new(10).to_s) + assert_equal('XIX', RomanNumeral.new(19).to_s) + assert_equal('XXXII', RomanNumeral.new(32).to_s) + assert_equal('LI', RomanNumeral.new(51).to_s) + end + + def test_to_i + assert_equal(3, RomanNumeral.from_string('III').to_i) + assert_equal(9, RomanNumeral.from_string('IX').to_i) + end + + def test_identity + (1..3999).each {|n| assert_equal(n, RomanNumeral.from_string(RomanNumeral.new(n).to_s).to_i)} end end end # expected results: -RomanNumeral.new(1).to_s # => 'I' -RomanNumeral.new(2).to_s # => 'II' -RomanNumeral.new(3).to_s # => 'III' -RomanNumeral.new(4).to_s # => 'IV' -RomanNumeral.new(5).to_s # => 'V' -RomanNumeral.new(6).to_s # => 'VI' -RomanNumeral.new(9).to_s # => 'IX' -RomanNumeral.new(10).to_s # => 'X' -RomanNumeral.new(19).to_s # => 'XIX' -RomanNumeral.new(32).to_s # => 'XXXII' -RomanNumeral.new(51).to_s # => 'LI' +# RomanNumeral.new(1).to_s # => 'I' +# RomanNumeral.new(2).to_s # => 'II' +# RomanNumeral.new(3).to_s # => 'III' +# RomanNumeral.new(4).to_s # => 'IV' +# RomanNumeral.new(5).to_s # => 'V' +# RomanNumeral.new(6).to_s # => 'VI' +# RomanNumeral.new(9).to_s # => 'IX' +# RomanNumeral.new(10).to_s # => 'X' +# RomanNumeral.new(19).to_s # => 'XIX' +# RomanNumeral.new(32).to_s # => 'XXXII' +# RomanNumeral.new(51).to_s # => 'LI' # bonus: -RomanNumeral.from_string('III').to_i # => 3 -RomanNumeral.from_string('IX').to_i # => 9 -RomanNumeral.from_string('IX').to_i # => 9 +# RomanNumeral.from_string('III').to_i # => 3 +# RomanNumeral.from_string('IX').to_i # => 9 +# RomanNumeral.from_string('IX').to_i # => 9 # given any arbitrary integer n: -n == RomanNumeral.from_string(RomanNumeral.new(n).to_s).to_i - - +# n == RomanNumeral.from_string(RomanNumeral.new(n).to_s).to_i # ======================================================================================== # Problem 2: Golden Ratio @@ -62,11 +168,38 @@ def self.from_string module Assignment08 def golden_ratio(precision) - # your implementation here + precision_threshold = BigDecimal.new("0.1") ** precision + + fib_prev = 0 + fib = 1 + fib_next = 1 + + begin + new_fib_next = fib + fib_next + fib_prev = fib + fib = fib_next + fib_next = new_fib_next + + ratio_prev = BigDecimal.new(fib.to_s)/BigDecimal.new(fib_prev.to_s) + ratio = BigDecimal.new(fib_next.to_s)/BigDecimal.new(fib.to_s) + end until (ratio - ratio_prev).abs < precision_threshold + + ratio.round(precision).to_s('F') + end + + class GoldenRatioTest < Minitest::Test + include Assignment08 + + def test_golden_ratio + assert_equal("1.62", golden_ratio(2)) + assert_equal("1.61803", golden_ratio(5)) + assert_equal("1.61803399", golden_ratio(8)) + assert_equal("1.6180339887499", golden_ratio(13)) + end end end # expected results: -golden_ratio(2) # => 1.62 -golden_ratio(5) # => 1.61803 -golden_ratio(8) # => 1.61803399 +# golden_ratio(2) # => "1.62" +# golden_ratio(5) # => "1.61803" +# golden_ratio(8) # => "1.61803399" 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; +}