Суммируйте область объекта с помощью хэша в Ruby - PullRequest
2 голосов
/ 24 мая 2010
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

Я пытаюсь получить структуру в массиве следующим образом:

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

Кстати, я делаю скрипт для ruby ​​для Google Sketchup

Ноесли я запускаю этот код, я получаю только

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'

Поскольку я привык использовать PHP и просто $array['myownassoc'] += bignumber; Но я думаю, что это неправильный подход при использовании Ruby?

Так что любая помощь в том, как мне нужно идти, будет хорошей.

Ответы [ 3 ]

7 голосов
/ 24 мая 2010

Проблема заключается в следующем:

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

Это (примерно) эквивалентно

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

Однако вы начинаете с summa как пустой хеш:

summa = Hash.new

Это означает, что всякий раз, когда вы сталкиваетесь с конкретным материалом в первый раз (и, очевидно, это будет уже в самой первой итерации цикла), summa[face.material.display_name] просто не существует.Итак, вы пытаетесь добавить число к чему-то, что не существует, что, очевидно, не может работать.

Быстрое решение состоит в том, чтобы просто инициализировать хэш значением по умолчанию, чтобы вместо него возвращалось что-то полезноеnil для несуществующего ключа:

summa = Hash.new(0)

Однако в код можно внести множество других улучшений.Вот как я бы это сделал:

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 }
}

Я считаю, что намного легче читать, вместо "зацикливаться на этом, но пропустить одну итерацию, если это произойдет, а также нене делайте этого, если это произойдет ".

На самом деле это обычный шаблон, который почти каждый Rubyist уже написал дюжину раз, так что у меня фактически был фрагмент кода, который мне нужно было только немного адаптировать.Тем не менее, я покажу вам, как я мог бы поэтапно реорганизовать ваш исходный код, если у меня еще не было решения.

Сначала давайте начнем со стиля кодирования,Я знаю, что это скучно, но это важно . То, что является фактическим стилем кодирования, не важно, важно то, что код соответствует , что означает, что один фрагмент кода должен выглядеть так же, как любой другой фрагмент кода,В этом конкретном случае вы просите сообщество Ruby предоставить вам бесплатную поддержку, поэтому вежливо по крайней мере отформатируйте код в стиле, к которому привыкли члены этого сообщества.Это означает стандартный стиль кодирования Ruby: 2 пробела для отступа, snake_case для имен методов и переменных, CamelCase для констант, которые ссылаются на модули или классы, ALL_CAPS для констант и так далее.Не используйте круглые скобки, если они не проясняют приоритет.

В вашем коде, например, вы используете иногда 3 пробела, иногда 4 пробела, иногда 5 пробелов, а иногда 6 пробелов для отступа, и все это ввсего 9 непустых строк кода!Ваш стиль кодирования не только не согласуется с остальным сообществом, он даже не соответствует его собственной следующей строке!

Давайте сначала исправим это:

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

Ах, намного лучше.

Как я уже упоминал, первое, что нам нужно сделать, это исправить очевидную проблему: заменить summa = {} (что, кстати, было бы идиоматическим способом написать это) на summa = Hash.new(0).Теперь код по крайней мере работает .

В качестве следующего шага я бы переключил назначение двух локальных переменных: сначала вы назначаете entities, затем вы назначаете summa,затем вы что-то делаете с entities и вам нужно посмотреть на три строки, чтобы выяснить, что было entities.Если вы переключите эти два параметра, использование и назначение entities будут рядом друг с другом.

В результате мы увидим, что entities назначено, затем сразу используется, а затем никогда больше не используется.Я не думаю, что это значительно улучшает читабельность, поэтому мы можем полностью от нее избавиться:

for face in Sketchup.active_model.entities

Далее следует цикл for.Это очень не-идиоматические в Ruby;Рубиисты сильно предпочитают внутренние итераторы.Итак, давайте переключимся на одно:

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

Одно из преимуществ, которое он имеет, заключается в том, что теперь face является локальным по отношению к телу цикла, тогда как раньше он просачивался в окружающую область.(В Ruby только собственные тела модулей, тела классов, тела методов, тела блоков и тела сценариев имеют собственную область видимости; тела циклов for и while, а также выражения if / unless / case выражения don 'т.)

Давайте перейдем к телу цикла.

Первая строка - это предложение охраны.Это хорошо, мне нравятся пункты охраны: -)

Вторая строка, ну, если face.material истина, она делает что-то, иначе ничего не делает, что означает, что цикл закончен. Итак, это еще одно охранное предложение! Тем не менее, он написан в полностью стиле, отличном от первого предложения охраны, прямо на одну строку над ним! Опять же, важна последовательность:

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

Теперь у нас есть два охранных пункта рядом друг с другом. Упростим логику:

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

Но теперь есть только один пункт охраны, охраняющий только одно выражение. Итак, мы можем просто сделать само выражение условным:

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

Тем не менее, это все еще некрасиво: мы зацикливаемся на некоторой коллекции, а затем внутри цикла мы пропускаем все элементы, которые мы не хотим зацикливать. Итак, если мы не хотим зацикливаться на них, мы должны зацикливаться на них в первую очередь? Разве мы не просто выбираем сначала «интересные» элементы, а затем просто зацикливаем их?

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

Мы можем упростить это. Если мы понимаем, что o.kind_of? C совпадает с C === o, тогда мы можем использовать фильтр grep, который использует === для сопоставления с образцом вместо select:

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

Наш фильтр select может быть дополнительно упрощен с помощью Symbol#to_proc:

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

Теперь вернемся к циклу. Любой, кто имеет некоторый опыт работы с языками высшего порядка, такими как Ruby, JavaScript, Python, C ++ STL, C #, Visual Basic.NET, Smalltalk, Lisp, Scheme, Clojure, Haskell, Erlang, F #, Scala и & hellip; практически любой современный язык сразу же распознает этот паттерн как катаморфизм, reduce, fold, inject:into:, inject или как бы там ни назывался ваш предпочитаемый язык.

Что делает reduce, так это то, что он "сводит" несколько вещей в одну вещь. Наиболее очевидным примером является сумма списка чисел: она сводит несколько чисел к одному числу:

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

[Примечание: в идиоматическом Ruby это будет написано так же, как [4, 8, 15, 16, 23, 42].reduce(:+).]

Один из способов обнаружить reduce, скрывающийся за петлей, состоит в поиске следующего паттерна:

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

В этом случае accumulator - это хеш summa.

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

И последнее, но не менее важное: мне не нравится это явное возвращение h в конце блока. Очевидно, мы могли бы написать это в одной строке:

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

Но я предпочитаю использовать Object#tap (он же K-комбинатор):

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 }
}

И это все!

2 голосов
/ 24 мая 2010

summa[face.material.display_name] возвращает nil по умолчанию, когда face.material.display_name не является существующим ключом.При создании хэша вы можете указать другое значение по умолчанию для возврата.Что-то вроде:

summa = Hash.new(0)
0 голосов
/ 26 декабря 2010

Просто примечание к вашему резюме областей лица - вы также должны учитывать, что группы / компоненты могут быть масштабированы, поэтому вам нужно использовать преобразования всей иерархии групп / компонентов, содержащих лицо, которое вы проверяете. Помните, что группы / компоненты также могут быть перекошены - так что это также необходимо учитывать.

...