ghammell'sBlog

A Headcount App With Class

What is a Ruby class?


As a beginner Rubyist, your capabilities will increase significantly once you understand how to build and manage classes. So, what is a class? Every object within ruby is part of a class. In IRB, if you were to run [1,2,3].class, the output would be Array. Likewise, if you run {:name => "Joe"}.class, the output will be Hash.

Every object exists within a class, and that class includes instance methods that can be run on objects of that class. For example, ".include?" is an instance method within the array class. Running [1,2,3].include?(3) yields 'True'. The code behind the ".include?" method is written within the scope of the Array class, and so the ".include?"" method can be run on any instance of an array.

Once you've built your own class, you can create instances of that class, just like [1,2,3] is an instance of Array. Within your class you can create methods that are unique to instances of that class. As a simple example, lets say we created a class called Cars. You might then define a method within that class called honk, that, when called on an instance of Cars, returns the string "Honk!".

In my current role as a finance analyst, one of the worst parts of my jobs is keeping track of headcount for the businesses that I support. I'm going to walk you through the creation of a class that will help manage employees and expenses in our fictional company Hello Mellon, Inc.

Creating a class and instances of that class


Creating a class is super simple. Lets call ours 'Headcount':

class Headcount
end

There you go! We've created a class called 'headcount'. Alllrightt! Now, lets create some instances of that class:

gary = Headcount.new
bob = Headcount.new
craig = Headcount.new


Initializing


Cool, now we've got three objects - gary, bob, and craig - that are each an instance of our Headcount class. Let's refer to these objects as 'employees' from now on. So, what can we really do with our employees at this point? Not a whole lot. Lets fix that! When managing headcount, it is important for each employee in the database to have a first name, last name, and start date. If we were building a real app, we'd want these attributes to be required inputs when creating a new employee in a system. In order to require certain attributes when a new instance of Headcount is created, we need to use the initialize method:

class Headcount

  def initialize(firstname, lastname, startdate)
  end

end

Now if we ran gary = Headcount.new, we'd receive this error message:

in `initialize': wrong number of arguments (0 for 3) (ArgumentError)

This message is basically saying "Hang on there partner! In order to create an instance of Headcount, you need to input some basic information! Three tidbits of information, in fact. You input zero!" And that's because we didn't initialize the 'gary' instance of Headcount, meaning, we didn't input the required attributes of 'gary' when creating 'gary'. Let's give 'gary' some attributes:

gary = Headcount.new("Gary", "Hammell", "8/4/2014")

No error? Great. So, we now have a working instance of Headcount, called 'gary'. Ok, so now what can we do with this? Again, not much yet! In any decent headcount system, you'll want to be able to easily see certain attributes of a specific employee. So, if we wanted to see gary's last name, how could we do that?

Instance Variables

Through the use of instance variables of course! An instance variable is similiar to any other variable, except that it is only available within the scope of the class that it is defined in. Let's create some, and then I'll walk you through it:

class Headcount

  def initialize(firstname, lastname, startdate)
    @firstname = firstname
    @lastname = lastname
    @startdate = startdate
  end

end

What the heck is this? Alright, each of the variables with an '@' in front of it is an instance variable. Putting the '@' in front of the variable name will allow us to access that variable from anywhere within the scope of the Headcount class. It'll be easier to see as we progress through this exercise, but think about it for a second. If we created a method outside of the Headcount class, and in that method we created a variable, that variable is not accessible from anywhere outside of that method. Inside a class, it's very useful to be able to access the same variables from different methods. Particularly when those variables are set equal to the required inputs, which is what we did above by setting each instance variable equal to the corresponding user input. Basically, now I can use the first name of the employee in any other method within Headcount, via the @firstname variable.

Instance Methods


Ok, so CAN WE DO ANYTHING YET? Not quite. We need to write some instance methods that, when called on an instance of Headcount, will perform some actions for us. There is no tag for an instance method like there is for an instance variable (the '@'), rather instance methods are just methods defined within a class, that can be called on specific instances of that class. Let's add a method called firstname below our initialize method:

class Headcount

  def initialize(firstname, lastname, startdate)
    @firstname = firstname
    @lastname = lastname
    @startdate = startdate
  end

  def firstname
    @firstname
  end

end

Wooohooo! We've got some functional code! Let's try it out. Run:

gary = Headcount.new("Gary", "Hammell", "8/4/2014")
puts gary.firstname

Gary

Gnarrrr! By calling the firstname method on 'gary', we are accessing the corrseponding code within the Headcount class. All this method does is return the instance variable @firstname, which was set equal to the user input! Likewise, we can define methods to return @lastname and @startdate:

class Headcount

  def initialize(firstname, lastname, startdate)
    @firstname = firstname
    @lastname = lastname
    @startdate = startdate
  end

  def firstname
    @firstname
  end

  def lastname
    @lastname
  end

  def startdate
    @startdate
  end

end

Now run:

gary = Headcount.new("Gary", "Hammell", "8/4/2014")
puts gary.firstname
puts gary.lastname
puts gary.startdate

Gary
Hammell
8/4/2014

Pretty awesome. I think we can simplify things, though.

attr_accessor


Let's say you messed up, and input the last name 'Hammell', even though my real last name is 'THUNDERCLAP'. You've already created 'gary', so how could we go about changing the last name? You might try:

gary.lastname = "THUNDERCLAP"

Except you'll get:

undefined method `lastname=' for # (NoMethodError)

You see, the problem with the simple methods we created above is that there is no way to adjust the output value of those methods. They're stuck on the instance variables that they return. One way around this is to define a new method:

def lastname=(lastname)
  @lastname=lastname
end

Notice the "=" in the method name. This method is basically allowing us to reset the @lastname instance variable by taking a new lastname as an input, and resetting the instance variable to that input. So now, if we ran:

gary = Headcount.new("Gary", "Hammell", "8/4/2014")
puts gary.lastname
gary.lastname = "THUNDERCLAP"
puts gary.lastname

We'd get:

Hammell
THUNDERCLAP

Great, but thats a lot of code just to be able to pull and change the last name. To simplify this task, we can use the attribute accessor methods. Using attribute accessor methods is a really easy way to access and modify instance variables within a class. We can essentially replace all of this code:

def lastname
  @lastname
end

def lastname=(lastname)
  @lastname=lastname
end

With this code:

attr_accessor :lastname

Let's do that for each of our simple methods. Our total code now looks like this:

class Headcount

  attr_accessor :firstname, :lastname, :startdate

  def initialize(firstname, lastname, startdate)
    @firstname = firstname
    @lastname = lastname
    @startdate = startdate
  end

end

Just to test, lets try this out again:

gary = Headcount.new("Gary", "Hammell", "8/4/2014")
puts gary.lastname
gary.lastname = "THUNDERCLAP"
puts gary.lastname

Output:

Hammell
THUNDERCLAP

Still working! Oh yeah. But how? Well, by using the syntax :lastname, for example, the attr_accessor method knows to toggle the @lastname instance variable. For each of the symbols listed after 'attr_accessor', the corresponding instance variables are accessed from the initialize method. Now you can call gary.lastname to receive an output, or you can perform gary.lastname = "THUNDERCLAP" to reset the last name instance variable.

Alright, lets step things up a notch.

More instance methods

We want this to be a well rounded headcount application, so lets require a couple extra inputs when creating a new employee: base salary, bonus, and grade level.

class Headcount

  attr_accessor :firstname, :lastname, :base, :bonus, :grade, :startdate

  def initialize(firstname, lastname, base, bonus, grade, startdate)
    @firstname = firstname
    @lastname = lastname
    @base = base
    @bonus = bonus
    @grade = grade
    @startdate = startdate
  end

end

As the manager of Hello Mellon, Inc. I want to be able to tell how much my employees are costing me in compensation expense each month. We now have base salary and bonus as required inputs, but what could we do to output the monthly cost of a specific employee? Lets write a method! Try this out (remember, this is all within the Headcount class):

def monthly_exp
  ben_tax_rate = 0.185
  (@base * (1+@bonus) * (1+ben_tax_rate) / 12).to_i
end

Not too bad! What we did here was create an instance method called 'monthly_exp' inside the Headcount class that we can now call on any new Headcount object we create. The code starts off by setting a benefits & tax rate of 18.5%. Then, in the following equation, it takes the value stored in the @base instance variable, and grosses it up by the annual bonus amount. That total value is then grossed up by the benefits and tax rate, and is then divided by 12 to get the monthly expense.

Let's try it out:

gary = Headcount.new("Gary", "Hammell", 500000, 0.3, 8, "8/4/2014")
#that's right, $500000 / year baby! ..maybe someday...

puts gary.monthly_exp

64187

Damn, that's a lot of cash money for one month. We could change the method to output the value as a string, formatted like '$64,187', but that would make it more difficult to access from other methods within the Headcount class. That's a challenge for another day.

Let's also create a method to calculate the daily expense of an employee, which uses the 'monthly_exp' method. Let's add this to our Headcount class:

def daily_exp
  monthly_exp * 12 / 365
end

And try:

gary.daily_exp

2110

Nice.

What if we wanted to see the amount of money that Hello Mellon will be paying for an employee for the rest of the year? Let's try out another method! This is going to get a little bit trickier. Before we write the method, lets switch a couple things up. First, lets add the code

require 'date'

to the top of our program. I won't get too into this, but basically this allows us to access additional methods that are already defined within a different class, Date. Now, lets also switch up our required parameters again. Lets break 'startdate' into three paramaters 'year', 'month', and 'day' in our initialize method. So, now our code looks like this:

require 'date'

class Headcount

  attr_accessor :firstname, :lastname, :base, :bonus, :grade, :startdate

  def initialize(firstname, lastname, base, bonus, grade, startyear, startmonth, startday)
    @firstname = firstname
    @lastname = lastname
    @base = base
    @bonus = bonus
    @grade = grade
    @startdate = startdate
  end

  def monthly_exp
    ben_tax_rate = 0.185
    (@base * (1+@bonus) * (1+ben_tax_rate) / 12).to_i
  end

  def daily_exp
    monthly_exp * 12 / 365
  end

end

Great. The only problem is that the @startdate instance variable in our initialize method is now set to a value that doesn't exist. Before, this variable was set to a string input, like "8/4/2014". Now, we want this instance variable to set equal to an actual date value, using the year, month, and day inputs. To do this, lets change the value of @startdate:

@startdate = Date.civil(startyear,startmonth,startday)

Now, upon initialization, we're creating an instance variable @startdate, that is set to an actual date value created by the Date.civil method. The Date.civil method takes three inputs - a year, a month, and a day - and returns a date value that functions like a number, as opposed to a string.

Ok, so our code looks like this:

require 'date'

class Headcount

  attr_accessor :firstname, :lastname, :base, :bonus, :grade, :startdate

  def initialize(firstname, lastname, base, bonus, grade, startyear, startmonth, startday)
    @firstname = firstname
    @lastname = lastname
    @base = base
    @bonus = bonus
    @grade = grade
    @startdate = Date.civil(startyear,startmonth,startday)
  end

  def monthly_exp
    ben_tax_rate = 0.185
    (@base * (1+@bonus) * (1+ben_tax_rate) / 12).to_i
  end

  def daily_exp
    monthly_exp * 12 / 365
  end

end

Alright! Remember that method we were going to work on, where it calculates the expense remaining in the year for an individual employee? Lets add this to our Headcount class:

def remaining_cost
  yearend = Date.civil(2014,12,31)
  cost = if @startdate < Date.today
             (yearend - Date.today) * daily_exp
         else (yearend - @startdate) * daily_exp
         end.to_i
end

What are we doing here? Well, first we define a new method called remaining_cost. From there, we set a variable 'yearend' equal to the last day of the year, using the Date.civil method. Then we run an if / else statement. If the employee started earlier than today, calculate the remaining cost between today and the end of the year. Else, calculate the remaining cost between the employee start date (which could be today or the future) and the end of the year.

Remember how I said that using 'Date.civil' creates a date value that functions like a number? That's why something like (yearend - Date.today) works. That equation returns the number of days between the two dates.

Let's try it:

p gary.remaining_cost

314390

(as of 7/8/2014.. also, disclaimer, these values might not be 100% because I am not accounting for cents)
Awesome. So now we've got three great methods to help us manage Hello Mellon's expenses. But each of these methods only provides us information on individual employees. What if we wanted a report of monthly expense by employee? Or what if we wanted to know our total employee count? For these functions, my friends, we need class methods

Class Methods

Class methods are methods that are called on classes themselves. Until now, we've been writing instance methods that are called on specific objects of a class, like gary.firstname. A class method would look something like Headcount.report. See how the method is called on the class itself? Lets jump right in to some applications.

In order to write effective class methods, we need some class variables. A class variable is a variable that is the same between all instances of a class. It is a global variable within the context of a single class, and its syntax requires '@@' in front of the variable name. Let's add the following to our code, just above the attr_accessor method:

@@employee_count = 0
@@employees = {}

What we're doing here is basically saying, 'upon inception of this class, create a class variable called employee_count, set to a value of 0. Create a class variable called employees, set equal to an empty hash'.

Let's also add the following code into our initialize method (don't worry, we'll cover everything here):

@id = (rand(1000) + 1).to_s.rjust(6,'0')
@@employee_count += 1
@@employees[@id] =
   {firstname: firstname,
   lastname: lastname,
   base: base,
   bonus: bonus,
   grade: grade,
   function: function,
   startdate: startdate}

Our code is getting longer!

require 'date'

class Headcount

  @@employee_count = 0
  @@employees = {}

  attr_accessor :firstname, :lastname, :base, :bonus, :grade, :startdate

  def initialize(firstname, lastname, base, bonus, grade, startyear, startmonth, startday)
    @firstname = firstname
    @lastname = lastname
    @base = base
    @bonus = bonus
    @grade = grade
    @startdate = Date.civil(startyear,startmonth,startday)
    @id = (rand(1000) + 1).to_s.rjust(6,'0')

    @@employee_count += 1

    @@employees[@id] =
       {firstname: firstname,
       lastname: lastname,
       base: base,
       bonus: bonus,
       grade: grade,
       function: function,
       startdate: startdate}
  end

  def monthly_exp
    ben_tax_rate = 0.185
    (@base * (1+@bonus) * (1+ben_tax_rate) / 12).to_i
  end

  def daily_exp
    monthly_exp * 12 / 365
  end

  def remaining_cost
    yearend = Date.civil(2014,12,31)
    cost = if @startdate < Date.today
               (yearend - Date.today) * daily_exp
           else (yearend - @startdate) * daily_exp
           end.to_i
  end

end

Alright, what did we do now? Well,



This may seem like a lot, so I highly recommend reviewing all of the above code to ensure that you understand it's purpose before continuing in this exercise.

Alright! Now lets make a method that spits out a report of all employees within Hello Mellon. Our first class method!! Notice how the method is defined using the syntax self.method_name:

def self.summary
   puts "Hello Mellon has #{@@employee_count} employees:"
   puts
      @@employees.each do |id, info|
         puts "ID: #{id}"
         info.each do |data, value|
            print "#{data}: #{value}; "
         end
         puts puts
   end
end

The intent of this method is to output a summary detailing each of our employees information. Until now, we've only created one employee, so lets create a few more:

gary = Headcount.new("Gary", "Hammell", 500000, 0.3, 8, 2014, 8, 4)
craig = Headcount.new("Craig", "Hammell", 300000, 0.25, 7, 2014, 10, 1)
bob = Headcount.new("Bob", "Hammell", 250000, 0.2, 7, 2014, 9, 2)
jack = Headcount.new("Jack", "Hammell", 400000, 0.3, 8, 2014, 11, 20)
angela = Headcount.new("Angela", "Hammell", 200000, 0.2, 6, 2014, 2, 28)
# It's a family company

Now, if we run:

Headcount.summary

We get:

Hello Mellon has 5 employees:

ID: 000764
firstname: Gary; lastname: Hammell; base: 500000; bonus: 0.3; grade: 8; startdate: 2014-08-04;

ID: 000362
firstname: Craig; lastname: Hammell; base: 300000; bonus: 0.25; grade: 7; startdate: 2014-10-01;

ID: 000222
firstname: Bob; lastname: Hammell; base: 250000; bonus: 0.2; grade: 7; startdate: 2014-09-02;

ID: 000964
firstname: Jack; lastname: Hammell; base: 400000; bonus: 0.3; grade: 8; startdate: 2014-11-20;

ID: 000679
firstname: Angela; lastname: Hammell; base: 200000; bonus: 0.2; grade: 6; startdate: 2014-02-28;

Schwiiiiing! How about if we wanted to report the remaining cost by employee, as well as a total?

Object Space


We might write something like:

def self.total_costs
   puts "Hello Mellon remaining headcount costs by employee for the current year:"
      puts
      total = ObjectSpace.each_object(Headcount).inject(0) do |result, employee|
         puts "#{employee.firstname} #{employee.lastname}: #{employee.remaining_cost}"
         result += employee.remaining_cost
   end
   puts
   puts "The total remaining headcount costs for the year is #{total}"
end

And by running:

Headcount.total_costs

We'd get:

Hello Mellon remaining headcount costs by employee for the current year:

Angela Hammell: 137104
Jack Hammell: 69208
Bob Hammell: 116760
Craig Hammell: 110747
Gary Hammell: 314390

The total remaining headcount costs for the year is 748209

See if you can figure out what ObjectSpace is. Honestly, I need to do more research myself. But the bigger question is, why did we have to use ObjectSpace to iterate through each object, as opposed to just iterating through the hash that contains all the employees? There are two reasons:

Thanks

I really hope you enjoyed this tutorial. There is a lot more that could be built into this class, and there is definitely room for improvement in my current code, but I think this is a good starting point. Hopefully someday I can build something like this into an awesome headcount app so that finance analysts like myself don't tear their hair out tying headcount out.

Thanks!!


Copyright: Gary Hammell 2014