Better way to do this?

Hi --

One of my tables has a recurring column name / type

id, description, ...., day1, day2, day3 ... day7

inside each of the days is an int.

Currently, I'm using send to loop over the day columns

total = 0 7.times do |i| total += row.send("day" + (i+1).to_s) end

I don't know how (in)efficient this is. Does anyone know a better way?

Also I was reading something about {} last night and it looks like I could write this as

total = 0 7.times do |i| { total += row.send("day" + (i+1).to_s) }

They both are pretty legible to me, but is there a major convention I should follow?

This kind of summing is often done with inject. Here's a[n untested] example:

   total = (1..7).inject(0) {|acc,day| acc + row.send("day#{i}") }

The idea is that the code block is run seven times, with an accumulator and the current day-number as arguments. The accumulator's value the first time through is 0. Each time through, the block returns the sum of the accumulator and the row's value for the current day. That value is then used as the value for the accumulator the next time through.

So by the seventh time, it's returning the sum of all of them -- and that's the final value of the whole inject expression, which then gets assigned to total.

David

wrote: Argh, beat me by a couple minutes David :slight_smile: Much better explanation though… but isn’t inject(0) redundant? Just inject {} does the trick because nil.to_i is 0… Cheers! Patrick

Hi --

Hi --

One of my tables has a recurring column name / type

id, description, ...., day1, day2, day3 ... day7

inside each of the days is an int.

Currently, I'm using send to loop over the day columns

total = 0 7.times do |i| total += row.send("day" + (i+1).to_s) end

I don't know how (in)efficient this is. Does anyone know a better way?

Also I was reading something about {} last night and it looks like I could write this as

total = 0 7.times do |i| { total += row.send("day" + (i+1).to_s) }

They both are pretty legible to me, but is there a major convention I should follow?

This kind of summing is often done with inject. Here's a[n untested] example:

   total = (1..7).inject(0) {|acc,day| acc + row.send("day#{i}") }

The idea is that the code block is run seven times, with an accumulator and the current day-number as arguments. The accumulator's value the first time through is 0. Each time through, the block returns the sum of the accumulator and the row's value for the current day. That value is then used as the value for the accumulator the next time through.

So by the seventh time, it's returning the sum of all of them -- and that's the final value of the whole inject expression, which then gets assigned to total.

Argh, beat me by a couple minutes David :slight_smile: Much better explanation though... but isn't inject(0) redundant? Just inject {} does the trick because nil.to_i is 0...

It does default to 0, though I don't think it's because of nil.to_i, since that implies that there's an automatic to_i performed on the accumulator, which there isn't; you can do:

   [1,2,3].inject() ...

for example.

I usually put the 0 in anyway just because it's sort of self-documenting that way.

David

Hi --

   total = (1..7).inject(0) {|acc,day| acc + row.send("day#{i}") }

Just looking this over, it sort of makes sense, but could you expand on this a little more?

(1..7).inject - number of times run, start and end values

And it can be any Enumerable (array, range, hash, string, your own, etc.).

(0) - initial value {|acc, - the internal variable that is assigned the init value day>} - ? not sure about is there, should it be i? acc + ... } - the code that is executed

Yes, you're right: day should be i in the block variable list. I got too cutesy with the variable names. Once again the 'untested' disclaimer comes to the rescue :slight_smile:

Does that answer all your questions? If not, let me know.

Perhaps I am making a leap here but if these are the same Davids -

David A. RUBY FOR RAILS by David A. Black (Ruby for Rails)

It's a pretty logical leap, and a correct one :slight_smile:

I've been working through your book lately. It's been very helpful and enlightening! Thanks for taking the time to write it.

Thanks -- glad to have done it!

David

Hi --

Here is hopefully my last tangent for the thread.

What if I wanted to assign a value dynamically?

cell = returnEditedCell(params)

row.send("day#{cell}") = newVal

211: syntax error, unexpected '=', expecting kEND row.send("day#{cell}") = values[:"d#{cell}"].to_f

The missing ingredient here is the fact that day1=, day2=, etc. are actually names of methods (assuming we're still talking about those). So you have to send the whole thing, plus any arguments, like this (untested):

   row.send("day#{cell}=", values[:"d#{cell}"].to_f)

I've sort of lost track of exactly what kind of object we're dealing with here, but there might be other things you can do involving addressing attributes directly. But that's the basic deal with the send semantics.

David

wrote: Should have read the docs better:

enum.inject(initial) {| memo, obj | block } => obj enum.inject {| memo, obj | block } => obj

The second form uses the first element of the collection as a the > initial value (and skips that element while iterating). So in the following the block is only called once: [1, 2].inject {|sum, i| “sum:#{sum} i:#{i}”} # => sum:1 i:2 This also means we need to specify the 0 in the original case or we will never get the actual value for day 1. Cheers! Patrick

Hi Alex,

What if I wanted to assign a value dynamically?

cell = returnEditedCell(params)

row.send("day#{cell}") = newVal

not tested but it should be :

row.send("day#{cell}=", newVal)

or

row.write_attribute("day#{cell}", newVal)

or

row[ :"day#{cell} ] = newVal

211: syntax error, unexpected '=', expecting kEND row.send("day#{cell}") = values[:"d#{cell}"].to_f

In fact, you want to use the method day1=, day2= ... and not day1, day2.

    -- Jean-François.

not to knock the inject way being discussed (i like inject, honest!) but there is a bit of a penalty for using an iterator versus just adding up the respective totals. since you know you only have the 7 columns in your record so why not just total them up within the model as an alternative? unless you plan on adding day columns to your table, the method should never change.

class MyModel < ActiveRecord::Base   def day_total     day1 + day2 + day3 + day4 + day5 + day6 + day7   end end

a quick test of inject vs unrolled shows (using the day1..day7 method described previously vs. day_total method described here)

10000 iterations inject: 0.568689000000041 unrolled: 0.119564999999983 diff: 0.449124000000058

on average, thats about a 1/2 sec difference on 10000 records. i know that's not a lot but it can make a difference when you have a reporting system and everyone wants their reports at month end! :slight_smile:

anyways, thats my $0.02 on the subject.

Hi --

It does default to 0, though I don't think it's because of nil.to_i, since that implies that there's an automatic to_i performed on the accumulator, which there isn't; you can do:

   [1,2,3].inject() ...

for example.

I usually put the 0 in anyway just because it's sort of self-documenting that way.

Should have read the docs better:

> enum.inject(initial) {| memo, obj | block } => obj > enum.inject {| memo, obj | block } => obj > > The second form uses the first element of the collection as a the > initial value (and skips that element while iterating).

So in the following the block is only called *once*:

[1, 2].inject {|sum, i| "sum:#{sum} i:#{i}"} # => sum:1 i:2

This also means we need to specify the 0 in the original case or we will never get the actual value for day 1.

So my instincts were right, but for the wrong reason :slight_smile: Thanks for the clarifying quote from the docs. Interesting that in most of the inject cases one sees, the 0-default theory sort of works by coincidence: if you're summing an array of numbers, the first one gets used as the basis anyway, and added to from then on; and most of the time that you're *not* using numbers, the seed value matters so you usually see one:

   ["a","b","c"].inject() {|arr,s| arr << s.upcase } # ["A","B","C"]    ["a","b","c"].inject {|arr,s| arr << s.upcase } # "aBC"

(Not a great real-world example, but anyway.)

David