I was wondering if something like this was possible?
info = arrange_info({|x| [x.name, x.number]}, info_array)
def arrange_info(block, info)
info.map(block).to_h
end
This would allow me to pass different blocks to arrange the array is different ways, how I have it now doesn't work, but is something like this possible?
A block can be passed as a method argument, but it needs to be the last one. You also cannot call a method before it has been defined :
def arrange_info(info, &block)
info.map(&block).to_h
end
info = arrange_info(info_array){|x| [x.name, x.number]}
Here's a small test :
class X
def initialize(name, number)
#name = name
#number = number
end
attr_reader :name, :number
end
def arrange_info(info, &block)
info.map(&block).to_h
end
info_array = [X.new('a', 1), X.new('b', 2)]
p info = arrange_info(info_array) { |x| [x.name, x.number] }
#=> {"a"=>1, "b"=>2}
Adding to Eric's answers.
These are equivalent
def arrange_info(info, &block)
info.map(&block).to_h
end
def arrange_info(info) # takes a block
info.map { |each| yield each }.to_h
end
The latter avoids materializing the block as an object.
Related
I'm working to create a few Ruby builder objects, and thinking on how I could reuse some of Ruby's magic to reduce the logic of the builder to a single class/module. It's been ~10 years since my last dance with the language, so a bit rusty.
For example, I have this builder:
class Person
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
def build
output = {}
PROPERTIES.each do |prop|
if self.respond_to?(prop) and !self.send(prop).nil?
value = self.send(prop)
# if value itself is a builder, evalute it
output[prop] = value.respond_to?(:build) ? value.build : value
end
end
output
end
def method_missing(m, *args, &block)
if m.to_s.start_with?("set_")
mm = m.to_s.gsub("set_", "")
if PROPERTIES.include?(mm.to_sym)
self.send("#{mm}=", *args)
return self
end
end
end
end
Which can be used like so:
Person.new(name: "Joe").set_age(30).build
# => {name: "Joe", age: 30}
I would like to be able to refactor everything to a class and/or module so that I could create multiple such builders that'll only need to define attributes and inherit or include the rest (and possibly extend each other).
class BuilderBase
# define all/most relevant methods here for initialization,
# builder attributes and object construction
end
module BuilderHelper
# possibly throw some of the methods here for better scope access
end
class Person < BuilderBase
include BuilderHelper
PROPERTIES = [:name, :age, :email, :address]
attr_accessor(*PROPERTIES)
end
# Person.new(name: "Joe").set_age(30).set_email("joe#mail.com").set_address("NYC").build
class Server < BuilderBase
include BuilderHelper
PROPERTIES = [:cpu, :memory, :disk_space]
attr_accessor(*PROPERTIES)
end
# Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").build
I've been able to get this far:
class BuilderBase
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
end
class Person < BuilderBase
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def build
...
end
def method_missing(m, *args, &block)
...
end
end
Trying to extract method_missing and build into the base class or a module keeps throwing an error at me saying something like:
NameError: uninitialized constant BuilderHelper::PROPERTIES
OR
NameError: uninitialized constant BuilderBase::PROPERTIES
Essentially the neither the parent class nor the mixin are able to access the child class' attributes. For the parent this makes sense, but not sure why the mixin can't read the values inside the class it was included into. This being Ruby I'm sure there's some magical way to do this that I have missed.
Help appreciated - thanks!
I reduced your sample to the required parts and came up with:
module Mixin
def say_mixin
puts "Mixin: Value defined in #{self.class::VALUE}"
end
end
class Parent
def say_parent
puts "Parent: Value defined in #{self.class::VALUE}"
end
end
class Child < Parent
include Mixin
VALUE = "CHILD"
end
child = Child.new
child.say_mixin
child.say_parent
This is how you could access a CONSTANT that lives in the child/including class from the parent/included class.
But I don't see why you want to have this whole Builder thing in the first place. Would an OpenStruct not work for your case?
Interesting question. As mentioned by #Pascal, an OpenStruct might already do what you're looking for.
Still, it might be more concise to explicitly define the setter methods. It might also be clearer to replace the PROPERTIES constants by methods calls. And since I'd expect a build method to return a complete object and not just a Hash, I renamed it to to_h:
class BuilderBase
def self.properties(*ps)
ps.each do |property|
attr_reader property
define_method :"set_#{property}" do |value|
instance_variable_set(:"##{property}", value)
#hash[property] = value
self
end
end
end
def initialize(**kwargs)
#hash = {}
kwargs.each do |k, v|
self.send("set_#{k}", v) if self.respond_to?(k)
end
end
def to_h
#hash
end
end
class Person < BuilderBase
properties :name, :age, :email, :address
end
p Person.new(name: "Joe").set_age(30).set_email("joe#mail.com").set_address("NYC").to_h
# {:name=>"Joe", :age=>30, :email=>"joe#mail.com", :address=>"NYC"}
class Server < BuilderBase
properties :cpu, :memory, :disk_space
end
p Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").to_h
# {:cpu=>"i9", :memory=>"32GB", :disk_space=>"1TB"}
I think no need to declare PROPERTIES, we can create a general builder like this:
class Builder
attr_reader :build
def initialize(clazz)
#build = clazz.new
end
def self.build(clazz, &block)
builder = Builder.new(clazz)
builder.instance_eval(&block)
builder.build
end
def set(attr, val)
#build.send("#{attr}=", val)
self
end
def method_missing(m, *args, &block)
if #build.respond_to?("#{m}=")
set(m, *args)
else
#build.send("#{m}", *args, &block)
end
self
end
def respond_to_missing?(method_name, include_private = false)
#build.respond_to?(method_name) || super
end
end
Using
class Test
attr_accessor :x, :y, :z
attr_reader :w, :u, :v
def set_w(val)
#w = val&.even? ? val : 0
end
def add_u(val)
#u = val if val&.odd?
end
end
test1 = Builder.build(Test) {
x 1
y 2
z 3
} # <Test:0x000055b6b0fb2888 #x=1, #y=2, #z=3>
test2 = Builder.new(Test).set(:x, 1988).set_w(6).add_u(2).build
# <Test:0x000055b6b0fb23b0 #x=1988, #w=6>
What's the idiomatic Ruby way of including a value in an array only if a condition is true?
class ItemPolicy
def initialize(user)
#user = user
#allowed = user.manager?
end
# Suggest improvements to the permitted_attributes method
def permitted_attributes
[:name, :details] + (#allowed ? [:price] : [])
end
end
This doesn't feel very Ruby-ish.
Nothing wrong with it, but I have a feeling that method might grow over time and get a bit more confusing. I'm not sure why #allowed is outside the method, but ignoring that I'd probably do this:
def permitted_attributes
permitted = [:name, :details]
permitted += :price if #allowed
permitted
end
That way you can grow it over time and add other logic, while keeping it readable.
Well, you could do something like this...
#allowed = false
def permitted_attributes
[
:name,
:details,
*(:price if #allowed),
]
end
Honestly that's kind of confusing in my opinion though. Really the best way is probably to just keep it simple:
def permitted_attributes
attrs = [:name, :details]
attrs << :price if #allowed
attrs
end
Like so:
def permitted_attributes
Array[
:name,
:details,
*#allowed ? :price : nil
]
end
or on one line if you prefer:
def permitted_attributes
[:name, :details, *#allowed ? :price : nil]
end
class Array
def add_object_if(object_to_add)
if yield
self << object_to_add
else
self
end
end
end
arr = [1,2]
bool = true
arr.add_object_if(3) { bool }
p arr #=> [1, 2, 3]
bool = false
arr.add_object_if(4) { bool }
p arr #=> [1, 2, 3]
The only thing I could could think of was maybe wrapping the conditional into a small method of its own, which provides a clearer context of why.
Also thought I'd add a attr_reader to remove the reuse of the instance variable.
class ItemPolicy
attr_reader :allowed
def initialize(user)
#user = user
#allowed = user.manager?
end
# Suggest improvements to the permitted_attributes method
def permitted_attributes
[:name, :details] + conditional_attributes
end
def conditional_attributes
return [] unless allowed
[:price]
end
end
I'm pretty new to Ruby(and programming in general), but I thought there was a way to call the attributes of all of a classes objects?
class Player
attr_reader :number
def initialize(name, number)
#name = name
#number = number
end
def self.all_numbers
[] << Player.each {|person| person.number}
end
end
guy1 = Player.new('Bill', 23)
guy2 = Player.new('jeff', 18)
I would like to just access the numbers for all objects by calling the class..
Player.all_numbers
hoping to return..
[23, 18]
The problem you have right now is that Player is your custom class. It does not respond to a class method each. Another issue is that the Player class has no knowledge of the instances created outside of it.
There's many ways to go about this. The way I would do this is to implement another class called Team like this
class Team
def initialize(*players)
#players = players
end
def player_numbers
#players.map { |player| player.number }
end
end
class Player
attr_reader :number
def initialize(name, number)
#name = name
#number = number
end
end
guy1 = Player.new('Bill', 23)
guy2 = Player.new('jeff', 18)
team = Team.new(guy1, guy2)
team.player_numbers
#=> [23, 18]
Write as below with the help of ObjectSpace#each_object :
Calls the block once for each living, nonimmediate object in this Ruby process. If module is specified, calls the block for only those classes or modules that match (or are a subclass of) module. Returns the number of objects found. Immediate objects (Fixnums, Symbols true, false, and nil) are never returned.
If no block is given, an enumerator is returned instead.
class Player
attr_reader :number
def initialize(name, number)
#name = name
#number = number
end
def self.all_numbers
ObjectSpace.each_object(self).map(&:number)
end
end
guy1 = Player.new('Bill', 23)
guy2 = Player.new('jeff', 18)
Player.all_numbers
# => [18, 23]
Another approach
class Player
attr_reader :number
#class_obj = []
def initialize(name, number)
#name = name
#number = number
self.class.add_object(self)
end
def self.add_object(ob)
#class_obj << ob
end
def self.all_numbers
#class_obj.map(&:number)
end
end
guy1 = Player.new('Bill', 23)
guy2 = Player.new('jeff', 18)
Player.all_numbers
# => [23, 18]
Use a Class Variable
Using a Team to hold references to complete Player objects is probably more desirable from a design perspective, but you can do what you want with a simple class variable. For example:
class Player
attr_reader :number
##numbers = []
def initialize(name, number)
#name = name
#number = number
##numbers << #number
end
def self.all_numbers
##numbers
end
end
guy1 = Player.new 'Bill', 23
guy2 = Player.new 'jeff', 18
Player.all_numbers
#=> [23, 18]
For this simple use case, that's probably sufficient. However, if you find yourself trying to stuff entire Player objects into a Player class variable then you will definitely want to find a better noun (e.g. Team or Players) to hold your collection.
I would simply override new and save the instances in a class variable:
class Player
attr_reader :number
def initialize(name, number)
#name = name
#number = number
end
def self.new(*args)
(##players ||= []) << super
##players.last
end
def self.all_numbers
##players.reduce([]) {|arr, player| arr << player.number}
end
end
Player.new('Bill', 23)
Player.new('jeff', 18)
p Player.all_numbers # => [23, 18]
Carpk, I should explain a few things:
Normally, Player.new('Bill', 23) would send the method Class#new to class Player, together with the two arguments. By defining self.new here, I am overriding Class#new. self.new invokes super, which invokes Class#new with self.new's arguments (there is no need to explicitly pass the arguments, unless you only want to pass some or none). Class#new returns the new class instance to self.new, which returns it as well, but first saves it in the array ##player.
(##players ||= []) is a typical Ruby trick. This has the effect of setting ##players equal to an empty array if it has not yet been defined (is nil), and leaves it unchanged if it already an array (empty or not). This is because ##players ||= [] is the same as ##players = ##players || []. If ##players => nil, this becomes ##players = []; else, ##players remains unchanged ([] is never evaluated). Cute, eh?
##players.last returns the class instance that was just returned by Class#new and added to the end of ##players.
##players.reduce([]) {|arr, player| arr << player.number} creates and returns an array arr of class instances, which in turn self.all_numbers returns. reduce's argument is the initial value of the accumulator, here an empty array named arr within the block. Note reduce and inject are synonyms.
Carpk, don't read any further; it will just be confusing.
I lied. I actually wouldn't do it that way. I suggested using a class variable and class methods because Carpk is new to Ruby and probably not into metaprogramming in a big way. What I'd actually do is define a class instance variable #players and make new and all_numbers singleton methods of the class Player. That way, instances of the class Player would not have easy access to that variable or those two methods. To do this, simply replace the above definitions of self.new and self.all_numbers with the following:
class << self
def new(*args)
(#players ||= []) << super
#players.last
end
def all_numbers
#players.reduce([]) {|arr, player| arr << player.number}
end
end
Carpk, I warned you.
For a pack of playing cards:
How can I use the suit hash (below) when creating a pack?
I have:
class PackOfCards
SUITS={H: 'Hearts', S:'Spades', D:'Diamonds', C:'Clubs'}
CARDS=['A','2','3','4','5','6','7','8','9','10','J','Q','K']
attr_accessor :pack_name, :cards
def initialize(pack_name)
#pack_name= pack_name
#cards = []
(0..3).each do |suit|
(0..12).each do |number|
#cards << PlayingCard.new(self, (SUITS[suit].value), CARDS[number])
end
end
end
end
class PlayingCard
attr_accessor :pack, :card_number, :card_suit
def initialize(pack, suit, number)
#card_suit = suit
#card_number = number
end
end
but I get:
pack_of_cards.rb:16:in `block (2 levels) in initialize':
undefined method `value' for
{:H=>"Hearts", :S=>"Spades", :D=>"Diamonds", :C=>"Clubs"}:Hash (NoMethodError)
Your SUITS is invalid expression. Perhaps you wanted to do this:
SUITS = %w[Hearts Spades Diamonds Clubs]
And it is not clear what you are doing, but perhaps you should be doing this:
#cards =
SUITS.flat_map{|suit| CARDS.map{|number| PlayingCard.new(self, suit, number)}}
Here is a corrected version, check the comments :
class PackOfCards
SUITS={H: 'Hearts', S:'Spades', D:'Diamonds', C:'Clubs'} # Use curly braces to define a hash, [] braces will define an array containing one hash
CARDS=['A','2','3','4','5','6','7','8','9','10','J','Q','K']
attr_accessor :pack_name, :cards
def initialize(pack_name)
#pack_name= pack_name
#cards = []
SUITS.each_key do |suit| # each_key is better since it gives you the key of the hash
(0..12).each do |number|
puts PackOfCards::SUITS[suit]
#cards << PlayingCard.new(self, (PackOfCards::SUITS[suit]), PackOfCards::CARDS[number]) # Call the hash with the right key to get the Suit
end
end
end
end
class PlayingCard
attr_accessor :pack, :card_number, :card_suit
def initialize(pack, suit, number)
#card_suit = suit
#card_number = number
end
end
Your Suit definition and lookup don't look valid.
How about something like this (assuming the output is a pack of cards with all suit and numbers) -
class PackOfCards
SUITS = ['Hearts', 'Spades', 'Diamonds', 'Clubs']
CARDS=['A','2','3','4','5','6','7','8','9','10','J','Q','K']
attr_accessor :pack_name, :cards
def initialize(pack_name)
#pack_name= pack_name
#cards = []
(0..3).each do |suit|
(0..12).each do |number|
#cards << PlayingCard.new(self, (SUITS[suit]), CARDS[number])
end
end
end
end
class PlayingCard
attr_accessor :pack, :card_number, :card_suit
def initialize(pack, suit, number)
#card_suit = suit
#card_number = number
end
end
You've actually put a hash in an array. To access the key, value pairs you'd have to access the array element first like this:
SUITS.first[:H]
In the following code, the issue is that after calling method .find_name on an object type of LogsCollection, the returned object becomes a native array and does not remain type LogsCollection. I believe the correct approach might be to create a constructor/initializer that accepts an array and return a brand new object of the correct type. But I am not sure there is not a better way to accomplish this?
Can a Ruby-pro eyeball this code and suggest (at the code level) the best way to make the returned object from .find_name remain type LogsCollection (not array)?
class Log
attr_accessor :name, :expense_id, :is_excluded, :amount, :paid_to
def initialize(name, expense_id, is_excluded, amount, paid_to)
#name = name
#expense_id = expense_id
#is_excluded = is_excluded
#amount = amount
#paid_to = paid_to
end
end
class LogsCollection < Array
def names
collect do |i|
i.name
end
end
def find_name(name)
#name = name
self.select { |l| l.name == #name }
end
end
logs = LogsCollection.new
logs.push(Log.new('Smith', 1, false, 323.95, nil))
logs.push(Log.new('Jones', 1, false, 1000, nil))
logs = logs.find_name('Smith')
puts logs.count
unless logs.empty?
puts logs.first.name # works since this is a standard function in native array
puts logs.names # TODO: figure out why this fails (we lost custom class methods--LogsCollection def find_name returns _native_ array, not type LogsCollection)
end
Final code post-answer for anyone searching (note the removal of base class < array):
class Log
attr_accessor :name, :expense_id, :is_excluded, :amount, :paid_to
def initialize(name, expense_id, is_excluded, amount, paid_to)
#name = name
#expense_id = expense_id
#is_excluded = is_excluded
#amount = amount
#paid_to = paid_to
end
end
class LogsCollection
attr_reader :logs
def initialize(logs)
#logs = logs
end
def add(log)
#logs.push(log)
end
def names
#logs.collect { |l| l.name }
end
def find_name(name)
LogsCollection.new(#logs.select { |l| l.name == name })
end
end
logs = LogsCollection.new([])
logs.add(Log.new('Smith', 1, false, 323.95, nil))
logs.add(Log.new('Jones', 1, false, 1000, nil))
puts logs.names
puts '--- post .find_name ---'
puts logs.find_name('Smith').names
As you can see in the docs Enumerable#select with a block always returns an array. E.g.
{:a => 1, :b => 2, :c => 3}.select { |k,v | v > 1 }
=> [[:b, 2], [:c, 3]]
What you could do is have some sort of constructor for LogsCollection that wraps up a normal array as a LogsCollection object and call that in find_name.
As requested here's an example class (I'm at work and writing this while waiting for something to finish, it's completely untested):
class LogsCollection
attr_reader :logs
def initialize(logs)
#logs = logs
end
def names
#logs.collect { |i| i.name }
end
def find_name(n)
name = n
LogsCollection.new(#logs.select { |l| l.name == n })
end
# if we don't know a method, forward it to the #logs array
def method_missing(m, *args, &block)
#logs.send(m, args, block)
end
end
Use like
lc = LogsCollection.new
logs = lc.logs.find_name('Smith')