Ruby array variable

I’m a beginner in Ruby.
I think my problem is my variable (list_pieces) whose values are not memorized by my Definition.
How do I do it? An expert can help me please?
Here is my code, which works with dynamic components whose sketchup file is saved.

New code below.

First of all, if you write in English it’s easier for everyone to understand and you’ll get more replies.

Regarding the Array it seems like you expect the method examiner_pieces to have access to the variable defined outside of it. In Ruby variables doesn’t work like this. Either you can pass the variable as an argument to examiner_pieces and the method will have access to the same Array object or you can make it a class or class instance variable.

Some general comments on the code: you don’t need parenthesis for methods that doesn’t take any arguments (e.g. “attribute_dictionaries()”) but for arguments that do take arguments it’s much recommended to have parenthesis around them to make the code easier to read (e.g. “UI.savepanel “Enregistrer rapport LDP”, modelPath, filename.chomp(”.skp")+“.csv”").

1 Like

Personally I find easier reading when using empty parenthesis for methods such as attribute_dictionaries().

It is probably because of my background which is heavy in C / C++. Regardless - empty parenthesis tells me that it is a method and not a reference to a class member. For the same reason when I define methods that do not have parameters I also use empty parenthesis. I also like to use them for if statements.

Ultimately it is a stylistic choice.

1 Like

Ok i have to pass the variable. I’ll try. But can someone tell me the way by giving examples? TY

1. There is only one return value.

So to make infos_piece return a list, you have to add elements to a temporary list and return that.

def infos_piece(pi)
    results = []
    pi.each{ |defs|
        # …
            results << tPiece
    }
    return results
end

2. Attaching a returned list to another list.

Also take care of the meaning of <<:
It inserts at the back (list.push(element)). To add another list to list, use list.concat(other_list).

liste_pieces = []
liste_pieces.concat(infos_piece(entites))

3. Passing a list as argument

Alternatively, you can pass the original list as (mutable) argument and insert elements into it instead of creating every time new temporary lists.

def ajoute_infos_piece(liste, pi)
    pi.each{ |defs|
        # …
            liste << tPiece
    }
end
# …
liste_pieces = []
ajoute_infos_piece(liste_pieces, entites)

4. Proper recursion end conditions

Recursion can perform a lot of work since it branches itself (exponential!). So you want it to stop somewhen and not cause a stack overflow.
Your recursion (examiner_Pieces(en)) only ends when it encounters an empty list (because then each does nothing and does not do more recursive calls). However, you assume everything contained in a list is iterable. Another abort condition is an element that is not iterable.

I would prefer to structure recursion that way:

def examiner_pieces(liste, ent)
    # Aborts if ent is nil or does not fulfill this condition
    if ent && ent.name == 'dynamic_attributes'
        ajoute_infos_piece(liste, ent)
        # Recurses only further if ent is iterable.
        if ent.respond_to?(:each)
            ent.each{ |en|
                examiner_pieces(liste, en)
            }
        end
    end
end
2 Likes

I try this right now, TY Aerilius.

I have tried everything according to my knowledge, nothing works. I’ll try again tomorrow.

It’s not a mistake in the code, but in your expectation: In your model, the model.entities collection contains only a single entity, so the code only outputs one line of the attributes of this entity.

If you want to traverse also entities nested in groups/components, you need to call your function again with the entities contained in the group/component. You can do this with recursion (see the code below). But there is one conceptional problem: when a component has multiple instances (n), you would visit the contained entities n times. If you don’t want that, you either have to keep track of all component definitions that you have already visited, or you traverse over model.definitions (linearly instead of recursion).

def infos_piece(entities)
    lines = []
    entities.each{ |entity|
        dicts = entity.attribute_dictionaries()
        next if dicts == nil
        # Read attributes from this entity
        infos = []
        dicts.each{ |dict|
            next if dict.name != 'dynamic_attributes'
            puts '', "dictionary: #{dict.name}, definition #{entity.name}"
            dict.each_pair{ |key, value|
                infos << sprintf("%s=%s",key,value)
            }
        }
        lines << infos.join(", ") unless infos.empty?
        # If the entity can contain other entities, recurse over the contained entities
        if entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
          lines.concat( infos_piece(entity.definition.entities) )
        end
    }
    return lines
end

           iPiece[0] = '' #Supprimer premier caractère (,)

Don’t do that! You are assuming a comma was added at the beginning of the string, but the comma was not added at the beginning, but after iPiece. Instead of adding and removing characters, it is safer not to add the comma unless you need it.

                iPiece += "," if iPiece.length != 0
                iPiece += sprintf("%s=%s",key,value)

or much better by joining a list with the character you want only to appear inbetween elements list.join(","). In programming you should never use assumptions, you must be able to prove that what you do is correct.

1 Like

Code UPDATED
I have simplify the code. And this work fine right now. (Code below)
My def “attrib_piece” is not the problem.

My problem it’s my def “lister_pieces”.
I need it to get ALL entities (also components in component) in the model.

My code list only the main components.
_has_movetool_behaviors=0.0,_lengthunits=INCHES,_name=CubesGroup,textgroup=group text example

Someone can show me how to get that?

def attrib_piece(defs)
    iPiece = "" #Infos piece
    dicts = defs.attribute_dictionaries
    if dicts != nil
        dicts.each{ |dict|
            if dict.name == 'dynamic_attributes'
                dict.each{ |key, value|
                    iPiece = iPiece + "," + key.to_s + '=' + value.to_s
                }
                iPiece[0] = '' #Supprimer premier caractère (,)
            end
        }
    end
    return iPiece
end
def lister_pieces(ents)
    lpieces = Array.new
    ents.each{ |ent|    
        lpieces << attrib_piece(ent)        
    }
    return lpieces
end
#-----
#START
#-----
mod = Sketchup.active_model
ents = mod.active_entities
les_pieces = Array.new
les_pieces.concat(lister_pieces(ents))
#Write file
modelPath = Sketchup.active_model.path
filename = File.basename(modelPath, '.csv')
if filename == ""
    filename = "Sans titre"
end
save_path = UI.savepanel "Enregistrer rapport CSV", modelPath, filename.chomp(".skp")+".csv"
if save_path != nil
    File.open(save_path, "w+") do |f|
      les_pieces.each {|element|f.puts(element)}
    end
end

Please don’t take it amiss, but my previous exactly solves this task:
A SketchUp model is a tree, not a sequential list. You only get top-level components if you don’t dive down into the tree (see entity.definition.entities).

And please fix iPiece[0] = '', it breaks your resulting string.

I know I’m noob in ruby… :confused:
Then my def “lister_pieces” will never be able the do it?

I mean… I have to get all pieces by (entity.definition.entities) ?

In my case…
iPiece[0] = ‘’ , really get out the first “,” of my line. Really it’s at the beginning of the line! :smile:
(Because the first time, iPiece is empty)

I try your def infos_piece and it give me only 1 result, why?

This give me only 1 result :confused:

def lister_pieces(ents)
    lpieces = Array.new
    ents.each{ |ent|    
        lpieces << attrib_piece(ent)
        if ent.respond_to?(:each)
            ent.each{ |en|
                lpieces.concat(lister_pieces(en))
            }
        end
    }
    return lpieces
end
> lpieces = Array.new
[]
> lpieces << 'item'
["item"]
> lpieces = Array.new # this will empty the array each time it runs
[]
> lpieces << 'item'
["item"]
> lpieces = Array.new if lpieces.empty? # this will not

> lpieces << 'item'
["item", "item"]
> lpieces = Array.new if lpieces.empty? # this will not

> lpieces << 'item'
["item", "item", "item"]

john

1 Like

It give me…
undefined method ‘empty?’ for nil:NilClass

sorry that will fail on the first one, before it exists…

 lpieces = Array.new unless lpieces.is_a?(Array)

should cover all tries…

john

1 Like

No problemo. But still give me only the first component! :frowning:

post your full code and an example model…

it’s hard to guess all the missing detail…

john

1 Like

here John…

In my report file « Component Cubes.csv » I need to see…

_has_movetool_behaviors=0.0,_lengthunits=INCHES,_name=CubesGroup,textgroup=group text example
_has_movetool_behaviors=0.0,_iscollapsed=false,_lengthunits=INCHES,_name=CubeA,integerten=10,stringcubea=CubeA
_has_movetool_behaviors=0.0,_iscollapsed=false,_lengthunits=INCHES,_name=CubeB,integertwenty=20,stringcubeb=CubeB
_has_movetool_behaviors=0.0,_lengthunits=INCHES,_name=CubeC,integerthirty=30,stringcubec=CubeC
_has_movetool_behaviors=0.0,_lengthunits=INCHES,_name=TinyCubeA,attribtesta=tCubeA
_has_movetool_behaviors=0.0,attribtestb=tCubeB
_has_movetool_behaviors=0.0,attribtestc=tCubeC
_has_movetool_behaviors=0.0,attribtestd=tCubeD
_has_movetool_behaviors=0.0,attribteste=tCubeE
_has_movetool_behaviors=0.0,attribtestf=tCubeF
_has_movetool_behaviors=0.0,attribtestg=tCubeG
_has_movetool_behaviors=0.0,attribtesth=tCubeH

:slight_smile: