DEV Community

Discussion on: Daily Challenge #4 - Checkbook Balancing

Collapse
 
mwlang profile image
Michael Lang

Ruby Language

Normally, I would build out classes for this sort of thing. A Checkbook class with Entries that can be Debit or Credit and so on so that it's easier to write and maintain tests in TDD style. This implementation is just a simplistic top-down functional approach. It's still testable, but it wasn't as easy to compose as an OOP solution would be.

script with specs

def clean line
  line.scan(/[\d\.a-z\s]/i).join
end

def etl line
  line.scan(/(\d+\s\D+)\s([\d\.]+)/).flatten
end

def compose_entry prev_balance, line
  entry = etl clean line
  debit = entry[1].to_f
  {
    prev_balance: prev_balance,
    memo: entry[0],
    debit: debit,
    new_balance: prev_balance - debit
  }
end

def report lines
  original_balance = clean(lines.shift).to_f
  balance = original_balance
  expenses = 0.0

  debits = lines.map do |line|
    entry = compose_entry balance, line
    balance = entry[:new_balance]
    expenses += entry[:debit]
    [
      entry[:memo],
      ("%0.2f" % entry[:debit]),
      "Balance",
      ("%0.2f" % entry[:new_balance]),
    ].join(" ")
  end

  [
    ("Original_Balance: %0.2f" % original_balance),
    debits,
    ("Total expense %0.2f" % expenses),
    ("Average expense %0.2f" % (expenses / debits.size)),
  ].join("\n")

end

require "spec"

DEMO_LEDGER = <<-DATA
1000.00
125 Market 125.45
126 Hardware 34.95
127 Video 7.45
128 Book 14.32
129 Gasoline 16.10
DATA
.split("\n").freeze

DEMO_ANSWER = <<-DATA
Original_Balance: 1000.00
125 Market 125.45 Balance 874.55
126 Hardware 34.95 Balance 839.60
127 Video 7.45 Balance 832.15
128 Book 14.32 Balance 817.83
129 Gasoline 16.10 Balance 801.73
Total expense 198.27
Average expense 39.65
DATA
.chomp.freeze

MESSY_LEDGER = <<-DATA
1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;
DATA
.split("\n").freeze

MESSY_ANSWER = <<-DATA
Original_Balance: 1233.00
125 Hardware 24.80 Balance 1208.20
123 Flowers 93.50 Balance 1114.70
127 Meat 120.90 Balance 993.80
120 Picture 34.00 Balance 959.80
124 Gasoline 11.00 Balance 948.80
123 Photos 71.40 Balance 877.40
122 Picture 93.50 Balance 783.90
132 Tires 19.00 Balance 764.90
129 Stamps 13.60 Balance 751.30
129 Fruits 17.60 Balance 733.70
129 Market 128.00 Balance 605.70
121 Gasoline 13.60 Balance 592.10
Total expense 640.90
Average expense 53.41
DATA
.chomp.freeze

describe "#clean" do
  let(:ledger) { MESSY_LEDGER.dup }
  it { expect(clean(ledger[0])).to eq "1233.00" }
  it { expect(clean(ledger[1])).to eq "125 Hardware 24.8" }
  it { expect(clean(ledger[6])).to eq "123 Photos 71.4" }
end

describe "#etl" do
  let(:ledger) { MESSY_LEDGER.dup }
  it { expect(etl(clean(ledger[1]))).to eq ["125 Hardware", "24.8"] }
  it { expect(etl(clean(ledger[10]))).to eq ["129 Fruits", "17.6"] }
end

describe "#compose_entry" do
  let(:ledger) { MESSY_LEDGER.dup }
  it { expect(compose_entry(1000, clean(ledger[1]))).to eq({ debit: 24.8, memo: "125 Hardware", new_balance: 975.2, prev_balance: 1000}) }
  it { expect(compose_entry(1000, clean(ledger[10]))).to eq({ debit: 17.6, memo: "129 Fruits", new_balance: 982.4, prev_balance: 1000}) }
end

describe "#report" do
  it {expect(report(DEMO_LEDGER.dup)).to eq DEMO_ANSWER}
  it {expect(report(MESSY_LEDGER.dup)).to eq MESSY_ANSWER}
end

output

>> rspec checkbook.rb
.........

Finished in 0.00648 seconds (files took 0.15208 seconds to load)
9 examples, 0 failures