Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2013 survey #1

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
136 changes: 114 additions & 22 deletions 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
5 changes: 5 additions & 0 deletions 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"
4 changes: 4 additions & 0 deletions 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"
60 changes: 48 additions & 12 deletions spec/salary_averager_spec.rb
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