diff --git a/lib/salary_averager.rb b/lib/salary_averager.rb index ac40a3e..c6ea585 100644 --- a/lib/salary_averager.rb +++ b/lib/salary_averager.rb @@ -1,46 +1,138 @@ require 'rubygems' -require 'faster_csv' +require 'csv' +# Usage: +# file_path = File.expand_path('data/data2013.csv', File.dirname(__FILE__)) +# puts SalaryAverager.new(file_path).salary_report class SalaryAverager - def initialize(path_to_data) - @data = FasterCSV.read(path_to_data) - @data.shift + class Averager < Array + def average + return nil if empty? + inject(&:+) / size + end + + def median + return nil if empty? + if size.odd? + sort[size / 2] + else + (sort[size / 2 - 1] + sort[size / 2]) / 2 + end + end + + def report + return too_small_message if sample_size_too_small? + "#{size} respondents. Average: #{average}. Median: #{median}." + end + + def sample_size_too_small? + size < 3 + end + + def too_small_message + case count = size + when 0 + "No respondents!" + when 1, 2 + "Sample size too small for anonymous reporting." + end + end + end + + + class MalformedSpreadsheetError < ArgumentError + def message + msg = "Spreadsheet must have column headers that include 'work status' or 'freelance'," + msg += "'manage' or 'tell other developers', and 'salary'.\n" + msg + super() + end + end + + + def initialize(file_path) + @data = CSV.read(file_path) + process_headers(@data.shift) + end + + def process_headers(headers) + @employment_column = headers.find_index { |entry| /(work status|freelance)/i =~ entry } + @manager_column = headers.find_index { |entry| /(manage|tell other developers)/i =~ entry } + @skill_column = headers.find_index { |entry| /(skill|ability)/i =~ entry } + @salary_column = headers.find_index { |entry| /salary/i =~ entry } + + unless @employment_column && @manager_column && @salary_column # skill column is optional + e = MalformedSpreadsheetError.new + raise e + end + end + + def entry_count + @data.size + end + + def freelancers + @data.select { |row| /(freelance|Yes)/i =~ row[@employment_column] } + end + + def non_freelancers + @data.reject { |row| /(freelance|Yes)/i =~ row[@employment_column] } + end + + def managers(data = @data) + data.select { |row| row[@manager_column] == "Yes" } + end + + def non_managers(data = @data) + data.select { |row| row[@manager_column] == "No" } + end + + def salaries_from(rows) + rows.map { |row| row[@salary_column].to_i } + end + + def skill_ratings + if @skill_column + @data.map { |row| row[@skill_column] }.map(&:to_f) + end end def freelancer_salaries - @data.select { |entry| entry[4] == "Yes" }.map { |entry| entry[1] }.map(&:to_f) + salaries_from(freelancers) end def non_freelancer_salaries - @data.select { |entry| entry[4] == "No" }.map { |entry| entry[1] }.map(&:to_f) + salaries_from(non_freelancers) end def manager_salaries - @data.select { |entry| entry[3] == "Yes" }.map { |entry| entry[1] }.map(&:to_f) + salaries_from(managers) end def non_manager_salaries - @data.select { |entry| entry[3] == "No" }.map { |entry| entry[1] }.map(&:to_f) + salaries_from(non_managers) end - def skill_ratings - @data.map { |entry| entry[2] }.map(&:to_f) + def non_manager_non_freelancer_salaries + salaries_from(non_managers(non_freelancers)) end -end - -class Array - def average - self.inject(&:+) / self.size.to_f + def manager_non_freelancer_salaries + salaries_from(managers(non_freelancers)) end -end + def report(salaries) + Averager.new(salaries).report + end -sa = SalaryAverager.new('data/data.csv') + def salary_report + "#{entry_count} people responded to the survey.\n" << + "Freelancers: #{report(freelancer_salaries)}\n" << + "Non-freelancers: #{report(non_freelancer_salaries)}\n" << + "Managers: #{report(manager_salaries)}\n" << + "Non-managers: #{report(non_manager_salaries)}\n" << + "Manager non-freelancers: #{report(manager_non_freelancer_salaries)}\n" << + "Non-manager non-freelancers: #{report(non_manager_non_freelancer_salaries)}" + end -puts "Average freelancer salary: %d" % sa.freelancer_salaries.average -puts "Average non-freelancer salary: %d" % sa.non_freelancer_salaries.average -puts "Average manager salary: %d" % sa.manager_salaries.average -puts "Average non-manager salary: %d" % sa.non_manager_salaries.average -puts "Average skill rating: %d" % sa.skill_ratings.average +end diff --git a/spec/fake_data2013.csv b/spec/fake_data2013.csv new file mode 100644 index 0000000..773fd6a --- /dev/null +++ b/spec/fake_data2013.csv @@ -0,0 +1,5 @@ +"Entry Id","What is your current annual salary? (Please estimate and include any annual bonuses).","Please rate your Rails ability from 1 (complete newbie) to 10 (Core Team Member).","Do you tell other developers what to do? (Are you a project manager or team lead?)","Are you a freelancer?","Any comments you'd like to make?","Date Created","Created By","Last Updated","Updated By","IP Address","Last Page Accessed","Completion Status" +"1","100000","6","Yes","Full-time, salaried employee","","2011-03-05 13:38:50","public","","","74.104.152.233","1","1" +"2","110000","6","No","Freelancer","","2011-03-05 13:38:50","public","","","74.104.152.233","1","1" +"3","120000","7","Yes","freelancer","","2011-03-05 13:39:50","public","","","74.104.152.234","1","1" +"4","130000","8","Yes","full-time, hourly","","2011-03-05 13:39:50","public","","","74.104.152.234","1","1" diff --git a/spec/numbers_only.csv b/spec/numbers_only.csv new file mode 100644 index 0000000..c80ff2c --- /dev/null +++ b/spec/numbers_only.csv @@ -0,0 +1,4 @@ +"1","100000","6","Yes","Full-time, salaried employee","","2011-03-05 13:38:50","public","","","74.104.152.233","1","1" +"2","110000","6","No","Freelancer","","2011-03-05 13:38:50","public","","","74.104.152.233","1","1" +"3","120000","7","Yes","freelancer","","2011-03-05 13:39:50","public","","","74.104.152.234","1","1" +"4","130000","8","Yes","full-time, hourly","","2011-03-05 13:39:50","public","","","74.104.152.234","1","1" diff --git a/spec/salary_averager_spec.rb b/spec/salary_averager_spec.rb index 0ce148f..04c73f4 100644 --- a/spec/salary_averager_spec.rb +++ b/spec/salary_averager_spec.rb @@ -3,45 +3,81 @@ require 'salary_averager' describe SalaryAverager do - before(:each) do - @averager = SalaryAverager.new("spec/fake_data.csv") - end + let(:averager) { SalaryAverager.new("spec/fake_data2013.csv") } describe "#freelancer_salaries" do it "returns all salaries for people who are freelancers" do - @averager.freelancer_salaries.should == [110000.0, 120000.0] + expect(averager.freelancer_salaries).to eq([110000, 120000]) end end describe "#non_freelancer_salaries" do it "returns all salaries for people who are not freelancers" do - @averager.non_freelancer_salaries.should == [100000.0, 130000.0] + expect(averager.non_freelancer_salaries).to eq([100000, 130000]) end end describe "#manager_salaries" do it "returns all salaries for people who manage other devs" do - @averager.manager_salaries.should == [100000.0, 120000.0, 130000.0] + expect(averager.manager_salaries).to eq([100000, 120000, 130000]) end end describe "#non_manager_salaries" do it "returns all salaries for people who do not manage other devs" do - @averager.non_manager_salaries.should == [110000.0] + expect(averager.non_manager_salaries).to eq([110000]) end end - describe "#skill_ratings" do + describe "#skill_ratings" do it "returns the skill ratings for everyone" do - @averager.skill_ratings.should == [6,6,7,8] + expect(averager.skill_ratings).to eq([6,6,7,8]) end end + it "errors on a spreadsheet without the right headers" do + expect { + SalaryAverager.new("spec/numbers_only.csv") + }.to raise_error + end + end -describe "Goofy Array monkeypatch" do - it "averages the contents of an array" do - [2,4,6].average.should == 4 +describe "SalaryAverager::Averager" do + it "averages correctly" do + expect(SalaryAverager::Averager.new([2,4,12]).average).to eq(6) + end + + it "calculates median correctly on odd-length arrays" do + expect(SalaryAverager::Averager.new([2,4,12]).median).to eq(4) + end + + it "calculates median correctly on even-length arrays" do + expect(SalaryAverager::Averager.new([2,4,12,48]).median).to eq(8) end + + it "averages for two, one, or no people" do + expect(SalaryAverager::Averager.new([2,12]).average).to eq(7) + expect(SalaryAverager::Averager.new([2]).average).to eq(2) + expect { SalaryAverager::Averager.new([]).average }.not_to raise_exception + expect(SalaryAverager::Averager.new([]).average).to be_nil + end + + it "calculates median for two, one, or no people" do + expect(SalaryAverager::Averager.new([2,12]).median).to eq(7) + expect(SalaryAverager::Averager.new([2]).median).to eq(2) + expect { + SalaryAverager::Averager.new([]).median + }.not_to raise_exception + expect(SalaryAverager::Averager.new([]).median).to be_nil + end + + + it "doesn't report on small arrays" do + expect(SalaryAverager::Averager.new([2,4]).report).to match(/too small/) + expect(SalaryAverager::Averager.new([2]).report).to match(/too small/) + expect(SalaryAverager::Averager.new([]).report).to match(/(no |none)/i) + end + end