DaveG
DaveG

Reputation: 1203

Using calculations in Ruby and Rails

First let me apologize for a seemingly easy question, but being new to rails, ruby and programming I feel like I've exhausted the "New to Rails" tutorials out there.

Here's what I'm up against.

I have a Users model and Institution Model that have a "has_many :through => :company_reps" relationship.

The user has basic fields (name, email, password) (I'm using devise)

The Institution has many fields but the relevant ones are (client = boolean, lead = boolean, demo_date = date/time) To complicate it further each Institution can have one or two users but most only have one.

We are holding a contest for the users and I need to award points to each user based on the demo_date field and client field.

So firstly what I need to do is give each user 10 points that is related to an institution that is a client, unless that institution has 2 users in which case I need to give those two users 5 points each.

Secondly I need to give all users 1 point that are related to an institution that has a demo date after Feb. 2012.

I'm using Ruby 1.9.2, Rails 3.2.8 and MySQL

As always thank you for the help.

MySQL Institution Info

CREATE TABLE `institutions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `state_id` int(11) DEFAULT NULL,
  `company` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `clientdate` datetime DEFAULT NULL,
  `street` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `city` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `zip` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `source` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `source2` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `demodate1` datetime DEFAULT NULL,
  `demodate2` datetime DEFAULT NULL,
  `demodate3` datetime DEFAULT NULL,
  `client` tinyint(1) DEFAULT NULL,
  `prospect` tinyint(1) DEFAULT NULL,
  `alead` tinyint(1) DEFAULT NULL,
  `notcontacted` tinyint(1) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7805 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 

Institution Model

class Institution < ActiveRecord::Base
  attr_accessible :company, :phone, :assets, :clientdate, :street, :city, :state_id, :zip, :source, :source2, :demodate1, :demodate2, :demodate3, :client, :prospect, :alead, :notcontacted
  belongs_to :state
  has_many :users, :through => :company_reps
  has_many :company_reps

end

User Model

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me, :first_name, :last_name
  # attr_accessible :title, :body

  has_many :states, :through => :rep_areas
  has_many :institutions, :through => :company_reps
  has_many :rep_areas
  has_many :company_reps

  def name 
    first_name + " " + last_name
  end


end

Company Rep Model

class CompanyRep < ActiveRecord::Base
  belongs_to :user
  belongs_to :institution
end

Upvotes: 0

Views: 169

Answers (2)

Andrew Haines
Andrew Haines

Reputation: 6644

Update (since my first attempt was wrongly assuming User has_one :institution

The simplest option would be to do the basic calculation on the Institution model to establish how many points the institution is "worth", and then sum that value to calculate the user's points.

# Institution
def points
  points_for_client + points_for_demo_date
end

private

def points_for_client
  if client?
    10 / users.count
  else
    0
  end
end

def points_for_demo_date
  if demo_date.present? && demo_date >= Date.new(2012, 3, 1)
    1
  else
    0
  end
end

Note that you could condense those if statements into one-liners with the ternary operator ? : if you prefer. Also note that I assumed "after February" to mean "from March 1 onwards".

The check for a nil demo_date is also a matter of taste. Take your pick from

# Verbose, but IMO intention-revealing
demo_date.present? && demo_date >= Date.new(...)

# Perhaps more idiomatic, since nil is falsy
demo_date && demo_date >= Date.new(...)

# Take advantage of the fact that >= is just another method
# Concise, but I think it's a bit yuk!
demo_date.try :>=, Date.new(...)

Now that each institution is worth a certain number of points, it's fairly simple to sum them up:

# User
def points
  institutions.inject(0) {|sum, institution| sum + institution.points }
end

Check out the docs for inject if you're not familiar with it, it's a nifty little method.

As far as performance goes, this is suboptimal. A basic improvement would be to memoize the results:

# Institution
def points
  @points ||= points_for_client + points_for_demo_date
end

# User
def points
  @points ||= institutions.inject ...
end

so that further calls of points in the same request don't recalculate the value. That's ok as long as the client and demo_date don't change while the User object is still alive:

some_user.points   #=> 0
some_user.institution.client = true
some_user.points   #=> 0 ... oops

The User object will be recreated the next request, so this might not be a problem (it depends on how these fields change).

You could also add a points field to the User and thus save the value in the database, using the original version as an update_points method instead

def update_points
  self.points = institutions.inject ...
end

However, working out when to recalculate the value is then going to be an issue.

My suggestion would be to keep it as simple as possible and avoid premature optimization. It's a relatively simple calculation so it's not going to be a big performance issue, as long as you don't have a huge number of users and institutions or a lot of requests going on.

Upvotes: 2

dpassage
dpassage

Reputation: 5453

Points accumulate to Users, so it would seem to make sense to add a method call on the User class which returns the number of points they've accumulated.

I'd start on this by just writing a method that computed the total points each time it's called, with some unit tests to make sure the computation is right. I wouldn't save the results at first - depending on how many objects you have, how often you need to compute points, etc, you may not need to save it at all.

Upvotes: 0

Related Questions