Question

require 'sketchup'

entities = Sketchup.active_model.entities
summa = Hash.new

for face in entities
  next unless face.kind_of? Sketchup::Face
  if (face.material)
    summa[face.material.display_name] += face.area
  end
end

I'm trying to get the structure in the array as such:

summa { "Bricks" => 500, "Planks" => 4000 }

By the way, I'm making a ruby script for Google Sketchup

But if I run this code I only get

Error: #<NoMethodError: undefined method `+' for nil:NilClass>
C:\Program Files (x86)\Google\Google SketchUp 7\Plugins\test.rb:17
C:\Program Files (x86)\Google\Google SketchUp 7\Plugins\test.rb:14:in `each'
C:\Program Files (x86)\Google\Google SketchUp 7\Plugins\test.rb:14
C:\Program Files (x86)\Google\Google SketchUp 7\Plugins\test.rb:8:in `call'

As I'm used to using PHP and just doing $array['myownassoc'] += bignumber; But I guess this isn't the right approach when using Ruby?

So any help in how I need to go would be nice.

Was it helpful?

Solution

The problem is this:

summa[face.material.display_name] += face.area

This is (roughly) equivalent to

summa[face.material.display_name] = summa[face.material.display_name] + face.area

However, you start out with summa as an empty hash:

summa = Hash.new

Which means that whenever you encounter a specific material for the first time (and obviously, this is going to be already the case in the very first iteration of the loop), summa[face.material.display_name] simply doesn't exist. So, you are trying to add a number to something that doesn't exist, which obviously cannot work.

The quick fix would be to just initialize the hash with a default value, so that it returns something useful instead of nil for a non-existing key:

summa = Hash.new(0)

There are, however, a lot of other improvements that could be made to the code. Here's how I would do it:

require 'sketchup'

Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).
reduce(Hash.new(0)) {|h, face|
  h.tap {|h| h[face.material.display_name] += face.area }
}

I find that much easier to read, instead of "loop over this, but skip one iteration if that thing happens, and also don't do this if that happens".

This is actually a common pattern, that pretty much every Rubyist has already written a dozen times, so I actually had a code snippet lying around that I only needed to slightly adapt. However, I am going to show you how I could have refactored your original code step-by-step if I hadn't already had the solution.

First, let's start with coding style. I know it's boring, but it is important. What the actual coding style is, is not important, the important thing is that the code is consistent, which means that one piece of code should look the same as any other piece of code. In this particular instance, you are asking the Ruby community to provide you with unpaid support, so it is polite to at least format the code in a style that members of that community are used to. This means standard Ruby coding style: 2 spaces for indentation, snake_case for method and variable names, CamelCase for constants which refer to modules or classes, ALL_CAPS for constants, and so on. Don't use parentheses unless they clear up the precedence.

In your code, for example, you use sometimes 3 spaces, sometimes 4 spaces, sometimes 5 spaces and sometimes 6 spaces for indentation, and all of that in just 9 non-empty lines of code! Your coding style is not only inconsistent with the rest of the community, it isn't even consistent with its own next line!

Let's fix that first:

require 'sketchup'
entities = Sketchup.active_model.entities
summa = {}

for face in entities
  next unless face.kind_of? Sketchup::Face
  if face.material
    summa[face.material.display_name] += face.area
  end
end

Ah, much better.

As I already mentioned, the first thing we need to do, is fix the obvious problem: replace summa = {} (which BTW would be the idiomatic way to write it) with summa = Hash.new(0). Now, the code at least works.

As a next step, I would switch the assignment of the two local variables: first you assign entities, then you assign summa, then you do something with entities and you have to look three lines up to figure out what entities was. If you switch the two, the usage and the assignment of entities are right next to each other.

As a result, we see that entities is assigned, then immediately used and then never used again. I don't think this improves readability much, so we can get rid of it altogether:

for face in Sketchup.active_model.entities

Next comes the for loop. Those are highly un-idiomatic in Ruby; Rubyists strongly prefer internal iterators. So, let's switch to one:

Sketchup.active_model.entities.each {|face|
  next unless face.kind_of? Sketchup::Face
  if face.material
    summa[face.material.display_name] += face.area
  end
}

One advantage this has, is that now face is local to the body of the loop, whereas before, it was leaking out into the surrounding scope. (In Ruby, only module bodies, class bodies, method bodies, block bodies and script bodies have their own scope; for and while loop bodies as well as if/unless/case expressions don't.)

Let's get on to the body of the loop.

The first line is a guard clause. That's good, I like guard clauses :-)

The second line is, well, if face.material is true-ish, it does something otherwise it does nothing, which means the loop is over. So, it's another guard clause! However, it is written in a totally different style than the first guard clause, directly one line above it! Again, consistency is important:

Sketchup.active_model.entities.each {|face|
  next unless face.kind_of? Sketchup::Face
  next unless face.material
  summa[face.material.display_name] += face.area
}

Now we have two guard clauses right next to each other. Let's simplify the logic:

Sketchup.active_model.entities.each {|face|
  next unless face.kind_of? Sketchup::Face && face.material
  summa[face.material.display_name] += face.area
}

But now there is only one single guard clause guarding only one single expression. So, we can just make the whole expression itself conditional:

Sketchup.active_model.entities.each {|face|
  summa[face.material.display_name] += face.area if
    face.kind_of? Sketchup::Face && face.material
}

However, that's still kind of ugly: we are looping over some collection, and then inside the loop we skip over all the items we don't want to loop over. So, if we don't want to loop over them, we do we loop over them in the first place? We don't we just select the "interesting" items first and then loop over just them?

Sketchup.active_model.entities.select {|e|
  e.kind_of? Sketchup::Face && e.material
}.each {|face|
  summa[face.material.display_name] += face.area
}

We can do some simplification on this. If we realize that o.kind_of? C is the same as C === o, then we can use the grep filter which uses === to pattern match, instead of select:

Sketchup.active_model.entities.grep(Sketchup::Face).select {|e| e.material
}.each { … }

Our select filter can further be simplified by using Symbol#to_proc:

Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).each { … }

Now let's get back to the loop. Anybody who has some experience in a higher-order language such as Ruby, JavaScript, Python, C++ STL, C#, Visual Basic.NET, Smalltalk, Lisp, Scheme, Clojure, Haskell, Erlang, F#, Scala, … basically any modern language at all, will immediately recognize this pattern as a catamorphism, reduce, fold, inject:into:, inject or whatever your language of choice happens to call it.

What a reduce does, is basically it "reduces" several things into just one thing. The most obvious example is the sum of a list of numbers: it reduces several numbers into just one number:

[4, 8, 15, 16, 23, 42].reduce(0) {|accumulator, number| accumulator += number }

[Note: in idiomatic Ruby, this would be written just as [4, 8, 15, 16, 23, 42].reduce(:+).]

One way to spot a reduce lurking behind a loop is to look for the following pattern:

accumulator = something # create an accumulator before the loop

collection.each {|element|
  # do something with the accumulator
}

# now, accumulator contains the result of what we were looking for

In this case, the accumulator is the summa hash.

Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).
reduce(Hash.new(0)) {|h, face|
  h[face.material.display_name] += face.area
  h
}

Last but not least, I don't like this explicit returning of h at the end of block. We could obviously write it on the same line:

h[face.material.display_name] += face.area; h

But I prefer the use of Object#tap (aka the K-combinator) instead:

Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).
reduce(Hash.new(0)) {|h, face|
  h.tap {|h| h[face.material.display_name] += face.area }
}

And, that's it!

OTHER TIPS

summa[face.material.display_name] returns nil by default when face.material.display_name isn't an existing key. When creating a hash, you can specify a different default value to return. Something like:

summa = Hash.new(0)

Just a note on your summary of face areas - you must also take into account of group/components might be scaled so you need make use of the transformations of the whole hierarchy of the groups/components containing the face you inspect. Remember that groups/components can also be skewed - so that has to be taken into account as well.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top