The Crystal Programming Language Forum

Can .select be used with classes?

https://play.crystal-lang.org/#/r/7qhl

class Zone
  property map_id = 0
  end


class Game
  property zones = Hash(Int32, Zone).new
  
  def initialize(zone_id)
    zones[zone_id] = Zone.new
  end
end


games = Hash(String, Game).new


10.times do |i|
  games["random_gamename#{i}"] = Game.new(i)
end

zones = games.select(Game).select(Zone)

https://crystal-lang.org/api/0.31.1/Enumerable.html#select(type:U.class)forallU-instance-method

Returns an Array with all the elements in the collection that are of the given type .

Since I’m using select with a hash, I’m pretty sure I’m doing it wrong.

I could write a custom method, but I’m thinking Crystal already has a built-in way of doing this

pp games.select { |_, v| v.is_a? Game}  
 
pp games["random_gamename1"].is_a? Game

Hmm, it’s true. Why doesn’t it select and return an array of all the Game elements in the Hash?

I feel like I’m getting closer :p

edit: Actually, I don’t even know if this is the correct way for what I am wanting to do.

I basically just want to save an each block (calling .each on games), and just iterate over the zones. I don’t like nested .each blocks, they get messy.

I’m not entirely sure what’s going on with Hash(K,V).select, but it looks like its splat signature is trying to process the class as a key, which obviously doesn’t work. Maybe someone with more Crystal experience than I have can make it work with something like games.select(Tuple(String, Game)), but it doesn’t look like that’s really what you want anyway.

Are you trying to call a method on each zone? If so, I suggest you add a method on Game that calls that method on each of its Zones. However, if you clarified exactly what you’re looking to do, it would help us help you.

1 Like

Okay, found out how to do it

def grab_zones(a)
  temp_zones = Array(Zone).new
  a.each do |k, v|
    v.zones.each do |k2, v2|
    	temp_zones << v2
    end
  end
  temp_zones
end

zones = grab_zones(games)

Or…

def grab_zones(a)
  temp_zones = Hash(Int32, Zone).new
  a.each do |k, v|
    v.zones.each do |k2, v2|
      temp_zones[k2] = v2
    end
  end
  temp_zones
end

However, I think @Blacksmoke16 or @asterite will be like oh god nooooo LOL

edit: Not sure about it returning an Array, it could be a Hash (which matches Hash(Int32, Zone))

Yeah, I just want to iterate of the zones while not having to iterate over them while I’m already inside a games.each block. In my main codebase, I do a lot of cleanup for items and entities (which are children of zones). I don’t like nested .each blocks, I wish there was a guard clause style alternative :laughing:

If the conditions for cleaning up items and entities within a Zone don’t depend on the other zones, then you can probably create a #cleanup method (or something similar) on Zone and a method like

class Game
  def clean_zones
    zones.each &.cleanup
  end
end

Hmm, I would have to create a cleanup method on my class Zone. And also another method clean_zones on my Game class. And then also call clean_zones every xx seconds. I’d rather just do this all in one place (for example, I use a separate file, CleanupTimers.cr)

OMG! I am so close

zones = games.map &.last.zones


pp typeof(zones)

Output:

Array(Hash(Int32, Zone))

How can I make this output Hash(Int32, Zone)??

I tried [0], but it’s returning the the Hash at index 0, not the zones Hash.

Does this accomplish what you’re aiming for?

class Zone
  property map_id = 0
end

class Game
  alias ZoneMap = Hash(Int32, Zone)
  property zones = ZoneMap.new
  
  def initialize(zone_id)
    zones[zone_id] = Zone.new
  end
end

games = Hash(String, Game).new

10.times do |i|
  games["random_gamename#{i}"] = Game.new(i)
end

game_zones = games.flat_map { |_name, game| game.zones }
zones = game_zones.each_with_object(Game::ZoneMap.new) do |zones, zone_map|
  zones.each do |id, zone|
    zone_map[id] = zone
  end
end

pp zones
1 Like

@jgaskins Yeah, nice. Would be nice to just do two .selects, but this works. Thanks!

I’m assuming each game can have more than 1 zone?

I’m thinking https://play.crystal-lang.org/#/r/7qmb would do the trick pretty easily.

Yeah

I want to iterate over the zones Hash property without actually being inside the games.each block.

Similar to how we can do guard clauses (and not have to be inside really deep if statements. But in this case, it’s not having to be inside .each blocks.

For example:

games.each do |game_name, game_obj|
    game_obj.zones.each do |zone_id, zone_obj|
        eww i'm inside a 2 level deep block
        zone_obj.loot.each do |itemid, item|
             eww i'm now inside a 3 level deep block!
         end
    end
end

I could just do:

zones = games.select(Game).select(Zone)
zones.each do |zone_id, zone_obj|
    Yay, now I only need to be inside one each block!
end

Would be super neat

@jgaskins’s solution does work though, so it’s not a deal-breaker. Just brainstorming outloud