Why Rails Can Use 'If' as Hash Key But Not in Ruby

Why Rails can use `if` as hash key but not in Ruby

IRb's parser is well-known to be broken. (In fact, the very bug you encountered was already reported months ago: Bug #12177: Using if: as symbol in hash with new hash syntax in irb console is not working.) Just ignore it. There are also other differences in behavior between IRb and Ruby, semantic ones, not just syntactic. E.g. methods defined at the top-level are implicitly public instead of implicitly private as they should be.

IRb tries to parse the code with its own parser to figure out, e.g. whether to submit it to the engine when you hit ENTER or wait for you on the next line to continue the code. However, because Ruby's syntax is extremely complex, it is very hard to parse it correctly, and IRb's parser is known to deviate from Ruby's.

Other REPLs take different approaches, e.g. Pry actually uses Ruby's parser instead of its own.

How to check if a specific key is present in a hash or not?

Hash's key? method tells you whether a given key is present or not.

session.key?("user")

Can't use hash.key in a ternary if statement?

Add parenthesis around parameter:

<%= @positions.key?('dashboard') ? 'true text' : 'no text' %>

Running Hash.new([]) does what you expect but not in the way you expect it

foo = Hash.new([]) sets the default value of a not existed key to an array. Herefoo[:bar] << 'Item 1' the :bar key doesn't exist, so foo uses an array to which you add a new element. By doing so you mutate the default value because the array is provided to you by a reference.


> foo = Hash.new([])
=> {}
> foo.default
=> []

If you call any not defined key on your hash you'll get this array:

> foo[:bar] << 'Item 1'
=> ["Item 1"]
> foo[:foo]
=> ["Item 1"]

To achieve your goal you should return a new array every time. It's possible by passing a block to Hash.new, the block will be executed every time you access a not defined key:

> foo = Hash.new { |hash, key| hash[key] = [] }
=> {}
> foo[:bar] << 'Item 1'
=> ["Item 1"]
> foo[:bar]
=> ["Item 1"]
> foo.keys
=> [:bar]
> foo[:foo]
=> []
> foo[:foo] << 'Item 2'
=> ["Item 2"]
> foo
=> {:bar=>["Item 1"], :foo=>["Item 2"]}

Here is the documentation.

Can I use AR object as hash key or should I use object_id instead

I think to use a hash is a good way to organise in your situation. However, I would advise against using the user or to big an object as hash keys, simply because it renders your hash unreadable and because it is really only this sole object with it's object id that can be used as a key.

o = Object.new
h = { o => 'something' }
h[Object.new] #=> nil

In your situation, this may not be an issue, because you simply need to iterate it. But it may be a shot in the leg as soon as you want to do something else with that hash, or you have different instances of the same Active Record Data (which is very common in Rails applications, unless you are a really paying attention what gets loaded when). Besides that, I think it is good to stick by the widely used convention to use simple objects (strings, symbols) as hash keys to make your code readable and maintainable.

Maybe it would be best to keep a two-dimensional hash, like this:

@users_with_things = Things.accessible_by(some_curent_user_logic).inject({}) do |a, thing|
user_id = thing.user.id
a[user_id] ||= { :user => thing.user, :things => [] }
a[user_id][:thing] << thing
a
end

Then you can iterate over @users_with_things in your view like this:

@users_with_things.each do |user_id, values|
# values[:user] is the user, values[:things] the array of things

Use value of key as part of value of another key in the same hash

What you ask for is not possible during Hash declaration. You can change your code like this:

purchase = {}
purchase[:product] = 'phone'
purchase[:quantity] = 5
purchase[:price] = 120
purchase[:total] = purchase[:quantity] * purchase[:price]

Or just the last line:

purchase = {
product: 'phone',
quantity: 5,
price: 120
}
purchase[:total] = purchase[:quantity] * purchase[:price]

Or other silly ways that involve slightly less typing:

purchase = {
product: 'phone',
quantity: 5,
price: 120
}.tap{ |h| h[:total] = h[:quantity] * h[:price] }

However, I would suggest that in a case like this you should NOT store "denormalized" data in your hash. As total is dependent upon the quantity or price, if either of them changes your hash will be invalid.

You could get around this by creating a special default_proc on your hash that computes the total on the fly each time it is requested:

purchase = {
product: 'phone',
quantity: 5,
price: 120
}
purchase.default_proc = ->(h,k){
h[:quantity]*h[:price] if k==:total
}
p purchase[:total] #=> 600
purchase[:quantity] = 7
p purchase[:total] #=> 840

However, it would be more clear to create a class or Struct to do this. A Struct is less code for you:

Purchase = Struct.new(:product,:quantity,:price) do
def total
quantity * price
end
end
purchase = Purchase.new('phone',5,120)
p purchase.total #=> 600
purchase.quantity = 3
p purchase.total #=> 360

However, the Struct does not (by default) allow keyword arguments. By writing your own class you can supply the arguments in any order, and even provide a default value:

class Purchase
attr_reader :product, :price, :quantity
def initialize(product:,price:,quantity:1) # default quantity
@product, @price, @quantity = product, price, quantity
end
def total
price*quantity
end
end

Purchase.new(price:10, product:'shoes').total #=> 10
Purchase.new(product:'shoes', price:10).total #=> 10
Purchase.new(quantity:3, price:10, product:'shoes').total #=> 30

Ruby/Rails return hash element if hash value is a string

What you need here is a recursion, i.e. a function, calling itself:

def filter_hash(hash)
hash.each_with_object({}) do |(key, value), acc|
acc[key] = value if value.is_a?(String)
acc[key] = filter_hash(value) if value.is_a?(Hash)
end
end

This function checks if the value of your hash is String, then it saves it, if it's a Hash it calls itself with that nested Hash

Fetch hash keys that are present in another array

The reason you attempt doesn't work is that hash keys can actually be any kind of object and Hash#slice will compare them by identity:

# its a hash with array keys - crazy stuff.
=> {[1, 2, 3]=>"a", [3, 4, 5]=>"b"}
irb(main):012:0> hash.slice([1,2,3])
=> {[1, 2, 3]=>"a"}
# [1,2,3] and [1,2] are not equal
irb(main):013:0> hash.slice([1,2])
=> {}
irb(main):014:0> hash.slice([3,4,5])
=> {[3, 4, 5]=>"b"}
irb(main):015:0>

An array will never be equal to a string in Ruby.

If you want to convert an array into a list of arguments to pass to a method use the splat operator (*):

irb(main):022:0> h = {"video"=>"MP4","audio"=>"MP3", "sharing"=>"NONE", "mix"=>"NONE"}
=> {"video"=>"MP4", "audio"=>"MP3", "sharing"=>"NONE", "mix"=>"NONE"}
irb(main):023:0> a = ["video", "audio", "txt"]
=> ["video", "audio", "txt"]
irb(main):024:0> h.slice(*a)
=> {"video"=>"MP4", "audio"=>"MP3"}


Related Topics



Leave a reply



Submit