Skip to content
136 changes: 114 additions & 22 deletions lib/salary_averager.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions spec/fake_data2013.csv
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 4 additions & 0 deletions spec/numbers_only.csv
Original file line number Diff line number Diff line change
@@ -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"
60 changes: 48 additions & 12 deletions spec/salary_averager_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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