Ruby minus operator with ActiveRecord objects - ruby-on-rails

Can you explain pls, how's Ruby's minus operator working? Not just trivial case like 5 - 2.
A bit more complex – we have 2 arrays with ActiveRecord objects:
Array A = User.where(...), Array B = User.where(...), I wanna make A - B, how is it working? Is it just comparing objects IDs, or all attributes, or smth else?

Is it just comparing objects IDs?
Type and ID, yes.
pry(main)> show-source User#eql?
From: /Users/sergio/.gem/ruby/2.5.1/gems/activerecord-5.2.0/lib/active_record/core.rb # line 420:
Owner: ActiveRecord::Core
Visibility: public
Number of lines: 6
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
!id.nil? &&
comparison_object.id == id
end

Just for additional details from Sergio's answer:
I narrowed down what the - method (operator) does on an ActiveRecord::Relation object, because I got curious as well myself:
Rails 5:
Traceback:
# rails console (pry-rails)
users_a = User.where(...)
users_b = User.where(...)
puts users_a.class
# => `User::ActiveRecord_Relation`
show-source users_a.-
# From: /Users/jrpolidario/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.2.0/lib/active_record/relation/delegation.rb # line 41:
# Owner: ActiveRecord::Delegation
# Visibility: public
# Number of lines: 4
#
# delegate :to_xml, :encode_with, :length, :each, :uniq, :join,
# :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
# :to_sentence, :to_formatted_s, :as_json,
# :shuffle, :split, :slice, :index, :rindex, to: :records
# since the `-` method as seen above is delegated to #records, let's see what the return type is the return value of `#records` is, of which is supposed to respond to the `-` operator.
puts users_a.records.class
# => Array
# ...because it's an Array type, then let's see if the Array type responds to the delegated `-` method.
show-source users_a.records.-
# From: array.c (C Method):
# Owner: Array
# Visibility: public
# Number of lines: 17
#
# static VALUE
# rb_ary_diff(VALUE ary1, VALUE ary2)
# {
# VALUE ary3;
# VALUE hash;
# long i;
#
# hash = ary_make_hash(to_ary(ary2));
# ary3 = rb_ary_new();
#
# for (i=0; i<RARRAY_LEN(ary1); i++) {
# if (st_lookup(rb_hash_tbl_raw(hash), RARRAY_AREF(ary1, i), 0)) continue;
# rb_ary_push(ary3, rb_ary_elt(ary1, i));
# }
# ary_recycle_hash(hash);
# return ary3;
# }
... which just simply means, I quote from Array
Returns a new array that is a copy of the original array, removing any items that also appear in other_ary. The order is preserved from the original array.
Rails 4
P.S. I also tried tracing this in rails 4.2, and show-source users_a.- does not show any method, which meant that it makes use of method_missing (therefore also meant that there has been changes between 4 and 5 with regards to this), and then, tracing further, I ended up with below:
Traceback:
[127, 136] in /Users/jrpolidario/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-4.2.10/lib/active_record/relation/delegation.rb
127:
128: def method_missing(method, *args, &block)
129: if #klass.respond_to?(method)
130: scoping { #klass.public_send(method, *args, &block) }
131: elsif array_delegable?(method)
=> 132: to_a.public_send(method, *args, &block)
133: elsif arel.respond_to?(method)
134: arel.public_send(method, *args, &block)
135: else
136: super
... which as you could see .to_a is called on the ActiveRecord::Relation object which means that it becomes an array, and then method (which is '-') is called on that Array object, which also means in the end it also calls the Array#- method same in Rails 5 above.

Related

How to call keys as method calls on hashes in ruby without OpenStruct

I am trying to allow to call:
x = { 'lol': 2 }
using methods:
x['lol]
and
x.lol
They should both return 2. I should be able to call any key in that hash which means I don't want to manually write a method for each key. I want to do this without OpenStruct.
Based on your comments, all keys will be Strings, which avoids any ambiguity between hash keys.
In its simplest form, you could build a class to handle this like so:
class HashMethodAccess
def initialize(h)
#_internal_hash = h
h.keys.each do |key|
define_singleton_method(key) { #_internal_hash[key] }
end
end
def [](val)
# alternatively public_send(val) if you would prefer NoMethodError
# rather than nil
# this would create a more unified interface but may cause some confusion
#_internal_hash[val.to_s]
end
end
usage
x = HashMethodAccess.new({'lol' => 2})
x.lol
#=> 2
# methods apply to each instance individually
y = HashMethodAccess.new({})
y.lol
#=> NoMethodError
BEWARE
While this can create methods from the keys not all methods can be called as x.method_name (You could write rules to handle some of these cases but that is beyond the scope of the question)
x = HashMethodAccess.new({'2lol' => 2, 'foo bar' => 27})
x.2lol
#=> SyntaxError
x.public_send("2lol")
#=> 2
x.foo bar
#=> NameError
bar = 12
x.foo bar
#=> NoMethodError (undefined method `foo'
x.send("foo bar")
#=> 27

Ruby Argument error in convert function

I am trying to run a program from a book that I am learning ruby from, but I keep getting this error no matter how I try to fix it
ex41.rb:70:in `convert': wrong number of arguments (0 for 2) (ArgumentError)
from ex41.rb:117:in `block (2 levels) in <main>'
from ex41.rb:113:in `each'
from ex41.rb:113:in `block in <main>'
from ex41.rb:108:in `loop'
from ex41.rb:108:in `<main>'
I do understand that I have the wrong number of arguments, but the thing is I can't find where I'm getting them wrong, could someone please explain this to me? For convienience I put (LINE XXX(as a number)) by the lines that cause me trouble. Thank you.
# calls external file from web
require 'open-uri'
# assigns WORD_URL to website
WORD_URL = "http://learncodethehardway.org/words.txt"
# creates empty array WORDS
WORDS =[]
# creates hash PHRASES
PHRASES = {
"class ### < ### \nend" =>
"Make a class named ### that is-a ###.",
# my assumption is that ### ### and the like will sb in words, lets find
out
"class ###\n\tdef initialize(###)\n\tend\nend" =>
"class ### has-a initialize that takes ### parameters.",
"class ###\n\tdef ***(###)\n\tend\nend" =>
"class ### has-a function named *** that takes ### parameters.",
"*** = ###.new()" =>
"Set *** to an instance of class ###.",
"***.***(###)" =>
"From *** get the *** function, and call it with parameters ###",
"***.*** = '***'" =>
"From ***, get the *** attribute and set it equal to '***'."
}
# creates variable for first argument if argument is english
PHRASE_FIRST = ARGV[0] == "english"
# opens WORD_URL function and creates function f
open(WORD_URL) {|f|
# for every line in f chomps break character and pushes out word to f
f.each_line {|word| WORDS.push(word.chomp)}
}
# defines function craft_names with variables rand_words, snippet and
pattern, and assigns caps value to false
def craft_names(rand_words, snippet, pattern, caps=false)
# assigns vriable names to snippet.scan on pattern maps and does the
following
function
names = snippet.scan(pattern).map do
# assigns word to rand_words that have popped off the end
word = rand_words.pop()
# Guessing again, maybe capitalizes the first word
caps ? word.capitalize : word
# ends do
end
# returns names twice
return names * 2
# ends craft_names function
end
# defines function craft_params with variables rand_words snippet, and
pattern
def craft_params(rand_words, snippet, pattern)
# assigns names to action scan with variable pattern with action
# length on beginning part of array to snippet and runs the following
# function using map
names = (0...snippet.scan(pattern).length).map do
# assigns variable param_count to an action that takes a random 3 and adds one to it
param_count = rand(3) + 1
# assigns variable params to one long ass action that maps 0 to param_count
value and pops out the last word
params = (0...param_count).map { |x| rand_words.pop() }
# joins params list, (I'm sure its a list) with string ,
params.join(", ")
# ends names for loop
end
# returns names twice
return names * 2
# ends craft_params function
end
# defines convert function with variables snippet and phrase
(LINE 70) def convert(snippet, phrase)
# sords words randomly and assigns words to rand_words
rand_words = WORDS.sort_by {rand}
# assigns class names to function craft mnames on rand_words, snippet /###/, and caps=true
class_names = craft_names(rand_words, snippet, /###/, caps=true)
# assigns other_names to craft_names with variables rand_words, snippet, and /\*\*\*/
other_names = craft_names(rand_words, snippet, /\*\*\*/)
# assigns param_names to craft_params with rand_words, snippet, and ### as variables
param_names = craft_params(rand_words, snippet, /###/)
# assigns empty array results
results = []
# on all variables snippet and phrase matchups perform the function
sentence
[snippet, phrase].each do |sentence|
# fake class names, also copies sentence
result = sentence.gsub(/###/) {|x| param_names.pop}
# fake other names
result.gsub!(/\*\*\*/)
# fake parameter lists
result.gsub!(/###/)
# returns result
results.push(result)
# ends function
end
# returns results
return results
# ends convert function
end
# keep going until they hit ctrl-d
# continual loop
(LINE 108)loop do
# assigns variable snippets to Prases.keys and sorts randomly
snippets = PHRASES.keys().sort_by {rand}
# for snippet in snippets
(LINE113) snippets.each do |snippet|
# assigns PHRASES on snippet to phrase
phrase = PHRASES[snippet]
# asssigns question and answer to converted snippet and phrase
#respectively
(LINE117)question, answer = convert[snippet, phrase]
# if values in phrase firs are equal to answer, question
if PHRASE_FIRST
# question, answer = answer, question
question, answer = answer, question
# ends if
end
# prints question, two line breaks, and > without line break
print question, "\n\n> "
# closes program unless input
exit(0) unless $stdin.gets
# prints line break ANSWER: answer line break line break
puts "\nANSWER: %s\n\n"
# ends unless
end
# ends loop
end
TL;DR – Use convert(snippet, phrase) instead of convert[snippet, phrase]
When you write
convert[snippet, phrase]
... it is equivalent to:
convert()[snippet, phrase]
Adding a space:
convert [snippet, phrase]
... would call the method with an array:
convert([snippet, phrase])
To actually call the method with two arguments, use:
convert(snippet, phrase)
or
convert snippet, phrase

Is there a lightweight way to lock down a set of keys on a Hash?

To be clear, I'm perfectly happy implementing this functionality as a custom class myself, but I want to make sure I'm not overlooking some bit of ruby or rails magic. I have googled every meaningful permutation of the keywords "ruby rails hash keys values immutable lock freeze". But no luck so far!
Problem: I need to give a Hash a set of keys, possibly at run time, and then lock the set of keys without locking their values. Something like the following:
to_lock = {}
to_lock[:name] = "Bill"
to_lock[:age] = 42
to_lock.freeze_keys # <-- this is what I'm after, so that:
to_lock[:name] = "Bob" # <-- this works fine,
to_lock[:height] # <-- this returns nil, and
to_lock[:height] = 175 # <-- this throws some RuntimeError
Question: Is there a bit of ruby or rails tooling to allow this?
I know of Object#freeze and of Immutable::Hash, but both lock keys and values.
Sticking with out-of-the-box ruby, the use case could be mostly met by manipulating the methods or accessors of classes at runtime, as in this or this, then overriding #method_missing. But that feels quite a bit clunkier. Those techniques also don't really "lock" the set of methods or accessors, it's just awkward to add more. At that point it'd be better to simply write a class that exactly implements the snippet above and maintain it as needed.
You can achieve this by defining a custom []= for your "to-lock" instance of a hash, after you've added the allowed keys:
x = { name: nil, age: nil }
def x.[]=(key, value)
# blow up unless the key already exists in the hash
raise 'no' unless keys.include?(key)
super
end
x[:name] # nil
x[:name] = "Bob" # "Bob"
x[:size] # nil
x[:size] = "large" # raise no
Note that this won't prevent you from inadvertently adding keys using something like merge!.
#meagar has offered an interesting solution, but has pointed out that it only works when attempting to add a key-value pair using Hash#[]. Moreover, it does not prevent keys from being deleted.
Here's another way, but it's rather kludgy, so I think you should probably be looking for a different way to skin your cat.
class Hash
def frozen_keys_create
self.merge(self) { |*_,v| [v] }.freeze
end
def frozen_keys_get_value(k)
self[k].first
end
def frozen_keys_put_value(k, new_value)
self[k].replace [new_value]
self
end
def frozen_keys_to_unfrozen
self.merge(self) { |*_,v| v.first }
end
end
Now let's put them to use.
Create a frozen hash with each value wrapped in an array
sounds = { :cat=>"meow", :dog=>"woof" }.frozen_keys_create
#=> {:cat=>["meow"], :dog=>["woof"]}
sounds.frozen?
#=> true
This prevents keys from being added:
sounds[:pig] = "oink"
#=> RuntimeError: can't modify frozen Hash
sounds.update(:pig=>"oink")
#=> RuntimeError: can't modify frozen Hash
or deleted:
sounds.delete(:cat)
#=> RuntimeError: can't modify frozen Hash
sounds.reject! { |k,_| k==:cat }
#=> RuntimeError: can't modify frozen Hash
Get a value
sounds.frozen_keys_get_value(:cat)
#=> "meow"
Change a value
sounds.frozen_keys_put_value(:dog, "oooooowwwww")
#=> {:cat=>["meow"], :dog=>["oooooowwwww"]}
Convert to a hash whose keys are not frozen
new_sounds = sounds.frozen_keys_to_unfrozen
#=> {:cat=>"meow", :dog=>"oooooowwwww"}
new_sounds.frozen?
#=> false
Add and delete keys
Maybe even add (private, perhaps) methods to add or delete key(s) to override the desired behaviour.
class Hash
def frozen_keys_add_key_value(k, value)
frozen_keys_to_unfrozen.tap { |h| h[k] = value }.frozen_keys_create
end
def frozen_keys_delete_key(k)
frozen_keys_to_unfrozen.reject! { |key| key == k }.frozen_keys_create
end
end
sounds = { :cat=>"meow", :dog=>"woof" }.frozen_keys_create
#=> {:cat=>["meow"], :dog=>["oooowwww"]}
new_sounds = sounds.frozen_keys_add_key_value(:pig, "oink")
#=> {:cat=>["meow"], :dog=>["woof"], :pig=>["oink"]}
new_sounds.frozen?
#=> true
newer_yet = new_sounds.frozen_keys_delete_key(:cat)
#=> {:dog=>["woof"], :pig=>["oink"]}
newer_yet.frozen?
#=> true
Sounds like a great use-case for the built-in Struct
irb(main):001:0> s = Struct.new(:name, :age).new('Bill', 175)
=> #<struct name="Bill", age=175>
irb(main):002:0> s.name = 'Bob'
=> "Bob"
irb(main):003:0> s.something_else
NoMethodError: undefined method `something_else' for #<struct name="Bob", age=175>
from (irb):3
from /home/jtzero/.rbenv/versions/2.3.0/bin/irb:11:in `<main>'

Ruby: Pass super into another method to execute conditionally

I have some code that looks like this:
if args
eval("super(#{args.join(',')})")
else
super
end
twice in a method. I'd like to move it so that my code looks more like:
def special_method
if a
some_block do
call_super(args, super_method)
end
else
call_super(args, super_method)
end
end
def call_super(args, super_method)
if args
eval("super(#{args.join(',')})")
else
super
end
end
I need to have a reference to the super I want to call (super special_method), since if I just create a method call_super and call super, it calls call_super on the superclass instead.
Does any of this make sense? x_x
It makes sense except for why you would ever need it. super already passes any parameters that the current method receives. super() passes no params. super(*args) passes any params in args, or no params if args is [] or nil.
If you actually want to do what your code currently does (pass args if they are non-nil, but current method's params if not) and not what I think you wanted, you can write args ? super(*args) : super as a short alternative (you can't put this in another method since it wouldn't know what the current parameters are).
(Also, you will find that in 99% of cases you think eval is the answer, there is a better answer.)
EDIT in response to the comment:
if args is ['testing', 1], then super(args) will pass one parameter that is an array; super(*args) passes two parameters (a string and an integer):
# a module
module Foo
def special_method
# multiple arguments in `args`
args = ['testing', 1]
super(*args)
end
end
class Bar
# fixed number of arguments (without splats):
def special_method(first, second)
puts "First parameter: #{first}"
puts "Second parameter: #{second}"
end
end
# subclass that includes the module
class Baz < Bar
include Foo
end
Baz.new.special_method
# First parameter: testing
# Second parameter: 1
(Note that "multiple arguments in *args" does not make sense, as *args is not a variable, args is).
I think one of the reasons for the confusion is the fact that splat has two different but related roles. In method definitions, they collect arguments into an array. Everywhere else, they distribute an array to an argument list.
require 'pp'
def foo(*args)
pp args
end
foo([1, 2]) # all the parameters (namely, the one) collected into `args`
# [[1, 2]]
foo(1, 2) # all the parameters (the two) collected into `args`
# [1, 2]
foo(*[1, 2]) # distribute `[1, 2]` to two parameters; collect both into `args`
# [1, 2]
def foo(args)
pp args
end
foo([1, 2]) # all the parameters (the one that exists) passed as-is
# [1, 2]
foo(1, 2) # all the parameters (the two) passed as-is, but method expects one
# ArgumentError: wrong number of arguments (2 for 1)
foo(*[1, 2]) # distribute `[1, 2]` to two parameters, but method expects one
# ArgumentError: wrong number of arguments (2 for 1)

How to set "programmatically"\"iteratively" each class object attribute to a value?

I am using Ruby on Rails 3.0.9 and RSpec 2. I am trying to refactoring some spec file in the following way (in order to test with less code similar class object attribute values):
[
:attribute_a,
:attribute_b,
:attribute_c
].each do |attr|
before do
# HERE I would like to set the "current" 'attr' related to the
# class object instance 'attribute_< letter >' (read below for
# more information) each time the iterator is called (note: all
# following attributes are NOT attr_accesible - for that reason
# I use the 'user.attribute_< letter >' in the 'before do'
# statement)
#
# # Iteration 1
# user.attribute_a = 'a_value'
# # No changes to 'user.attribute_b'
# # No changes to 'user.attribute_c'
#
# # Iteration 2
# # No changes to 'user.attribute_a'
# user.attribute_b = 'a_value'
# # No changes to 'user.attribute_c'
#
# # Iteration 3
# # No changes to 'user.attribute_a'
# # No changes to 'user.attribute_b'
# user.attribute_c = 'a_value'
# Maybe I should make something like the following but that, as well
# as the 'send' method must be used, doesn't work (the below code-line
# is just an example of what I would like to do).
#
# user.send(:"#{attr}") = 'a_value'
end
...
end
How can I improve the above code so to accomplish what I aim (I refer to the user.send(:"#{attr}") = 'a_value' part in order to set "programmatically" - that is, set a different attribute value for each iteration made - each user attribute value to 'a_value')?
You should use .send and append an = to the method name to invoke the setter, passing the value as the second argument to send:
[
:attribute_a,
:attribute_b,
:attribute_c
].each do |attr|
before do
user.send("#{attr}=", 'a_value')
end
You're effectively doing this:
user.send('attribute_a=', 'a_value');
Your syntax (user.send(:"#{attr}") = 'a_value') is wrong/weird for a couple reasons:
There's no reason to convert :attr to a string and them immediately back to a symbol
You can't assign a value to the return value of .send

Resources