Map splat arguments over method parameters - ruby

We create a method with splatted arguments and call Method#parameters on it:
def splatter(x, *y, z); end
params = method(:splatter).parameters
# => [[:req, :x], [:rest, :y], [:req, :z]]
I'm looking for a function f that will map a list of arguments onto their corresponding variable names. The function should be flexible enough to work on any other method with arbitrarily placed splat arguments. For example:
args = [:one, :two, :three, :four]
f(params, args)
# => [[:x, :one], [:y, :two], [:y, :three], [:z, :four]]
or something along those lines (flipped elements would be fine also). I feel there must be a flexible, elegant solution using inject or something, but I can't seem to come up with it.

def f(params,*args)
# elements to be assigned to splat parameter
splat = args.count - params.count + 1
# will throw an error if splat < 0 as that means not enough inputs given
params.map{ |p|
[ p[1] , ( p.first == :rest ? args.shift(splat) : args.shift ) ]
}
end
Examples
def splatter(x,*y,z)
# some code
end
f(method(:splatter).parameters, 1,2,3,4)
#=>[[:x, 1], [:y, [2, 3]], [:z, 4]]
def splatter(x,y,*z)
# some code
end
f(method(:splatter).parameters, 1,2,3,4)
# => [[:x, 1], [:y, 2], [:z, [3, 4]]]
def splatter(x,*z)
# some code
end
f(method(:splatter).parameters, 1)
# => [[:x, 1], [:z, []]]

I think this is a good example where eval can be useful. The code below generates a lambda which takes the same arguments as specified and spits out the resolved list of arguments. The advantage of this approach is that Ruby's own algorithm for resolving splats is used.
def resolve(parameters,args)
param_list = parameters.map do |type,name|
splat = '*' if type == :rest
"#{splat}#{name}"
end.join(',')
source = ""
source << "->(#{param_list}) do\n"
source << " res = []\n"
parameters.each do |type,name|
if type == :rest
source << " res += #{name}.map {|v| [:'#{name}',v] }\n"
else
source << " res << [:'#{name}',#{name}]\n"
end
end
source << "end"
eval(source).call(*args)
end
Example:
params = ->(x,*y,z){}.parameters
resolve(params,[:one, :two, :three, :four])
#=> [[:x, :one], [:y, :two], [:y, :three], [:z, :four]]
Behind the scenes, the following code was generated:
->(x,*y,z) do
res = []
res << [:'x',x]
res += y.map {|v| [:'y',v] }
res << [:'z',z]
end
Another example with two arguments, splat first:
params = ->(*x,y){}.parameters
resolve(params,[:one, :two, :three, :four])
#=> [[:x, :one], [:x, :two], [:x, :three], [:y, :four]]
With the generated code being
->(*x,y) do
res = []
res += x.map {|v| [:'x',v] }
res << [:'y',y]
end

Edit: After my initial confusion:
def doit(params, args)
rest_ndx = params.map(&:first).index(:rest)
to_insert = [params[rest_ndx].last]*(args.size-params.size) if rest_ndx
params = params.map(&:last)
params.insert(rest_ndx,*to_insert) if rest_ndx
params.zip(args)
end

Related

word_count(s) > Homework for counting letters in a text

My homework is to count the letters in a string regardless of the upper or lower case ... so far I have this which I still don't make it work, ideas?
def self.word_count_from_file(filename)
s = File.open(filename) { |file| file.read }
word_count(s)
end
def self.words_from_string(s)
s.downcase.scan(/[\w']+/)
end
def self.count_frequency(character)
counts = Hash.new(0)
for chatacter in characters
counts[character] += 1
end
# counts.to_a.sort {|a,b| b[1] <=> a[1]}
# sort by decreasing count, then lexicographically
counts.to_a.sort do |a,b|
[b[1],a[0]] <=> [a[1],b[0]]
end
end
Supposing you need to count words and not characters, I guess you expect to call the class as:
WordCount.word_count_from_string('Words from this string of words')
or
WordCount.word_count_from_file('filename.txt')
Then you need two class methods calling other methods in order to get the result. So, this is one option to make it work:
class WordCount
def self.word_count_from_file(filename)
s = File.open(filename) { |file| file.read }
count_frequency(s)
end
def self.word_count_from_string(s)
count_frequency(s)
end
def self.words_array(s)
s.downcase.scan(/[\w']+/)
end
def self.count_frequency(s)
counts = Hash.new(0)
for character in words_array(s) # <-- there were a typo
counts[character] += 1
end
counts.to_a.sort do |a,b|
[b[1],a[0]] <=> [a[1],b[0]]
end
end
end
WordCount.word_count_from_string('Words from this string of words')
#=> [["words", 2], ["from", 1], ["of", 1], ["string", 1], ["this", 1]]
WordCount.word_count_from_file('word-count.txt')
#=> [["words", 2], ["this", 1], ["in", 1], ["of", 1], ["string", 1], ["a", 1], ["from", 1], ["file", 1]]
Note that both word_count_from_file and word_count_from_string call count_frequency which calls words_array in order to get and return the result.
To be more Ruby-ish (each) and less Pythonic (for), this is an alternative version using also instance variable (#s) in order to avoid passing parameters (count_frequency instead of count_frequency(s), etc.).
class WordCount
def self.word_count_from_file(filename)
#s = File.open(filename) { |file| file.read }
count_frequency
end
def self.word_count_from_string(str)
#s = str
count_frequency
end
def self.count_frequency
words_array.each_with_object(Hash.new(0)) { |word, cnt| cnt[word] += 1 }.sort_by(&:last).reverse
end
def self.words_array
#s.downcase.scan(/[\w']+/)
end
end
Call as before.

Mutating an array of symbols

I want to mutate an array of symbols by adding an e or an s to the end of the symbols depending on the last letter of each symbol. For example, the array:
[:alpha, :beta, :kappa, :phi]
will be modified to:
[:alphae, :betae, :kappae, :phis]
I can do it using an if ... else condition and a regex with an array of strings, but not with symbols. I tried to convert my symbols to strings, mutate them, then convert back, but I get an error
s = [:aplha, :beta, :kappa, :phi]
def pluralSym(sym, out = [])
sym.each do |s|
s.to_s
if s.match(/a$/)
out = s.sub(/a$/, "ae")
elsif s.match(/i$/)
out = s.sub(/i$/, "is")
else
out = s
end
out.to_sym
end
end
p pluralSym(s)
block in pluralSym': undefined method `sub' for :aplha:Symbol
You can create a method that receives the symbol, the if that matches with /a$/ or /i$/, interpolate the suffix, and converts that to a symbol in each case, otherwise just return sym
def plural_sym(sym)
return "#{sym}ae".to_sym if sym =~ /a$/
return "#{sym}is".to_sym if sym =~ /i$/
sym
end
p [:aplha, :beta, :kappa, :phi].map(&method(:plural_sym))
# [:aplhaae, :betaae, :kappaae, :phiis]
The (&method(:plural_sym)) is just a way to call your function passing as argument each element within the block.
Notice here, you're not mutating an array, you're returning a new one.
You convert symbol to string, but you don't assign it and you keep using symbol. Also use map instead of each. A quickfix would be:
s = [:aplha, :beta, :kappa, :phi]
def pluralSym(sym, out = [])
sym.map! do |s|
str = s.to_s
if str.match(/a$/)
out = str.sub(/a$/, "ae")
elsif s.match(/i$/)
out = str.sub(/i$/, "is")
else
out = str
end
out.to_sym
end
end
H = { 'a'=>'e', 'i'=>'s' }
def plural_sym(arr)
arr.map! { |sym| (sym.to_s + H.fetch(sym[-1], '')).to_sym }
end
arr = [:aplha, :beta, :phi, :rho]
plural_sym arr
#=> [:aplhae, :betae, :phis, :rho]
arr
#=> [:aplhae, :betae, :phis, :rho]
See Hash#fetch.
A variant of this follows.
H = Hash.new { |h,k| '' }.merge('a'=>'e', 'i'=>'s')
def plural_sym(arr)
arr.map! { |sym| (sym.to_s + H[sym[-1]]).to_sym }
end
arr = [:aplha, :beta, :phi, :rho]
plural_sym arr
#=> [:aplhae, :betae, :phis, :rho]
arr
#=> [:aplhae, :betae, :phis, :rho]
See Hash::new.
Symbols are immutable in ruby so you need convert them to string first
s = s.to_s

Check to see if array contains string or int and complete if/else statement

Please help to explain what is needed in my code to decipher if the array contains an integer, if it does I need to add 1 to it and if doesn't if will just display the string or symbol.
I have left #notes where my brain stopped working
# possible arrays
# array = [1, "two", :three]
# array = [1, 2, 3]
class Array
def new_map
result = []
self.each do |item|
yield(item)
if #check to see if item is an integer
result << item + 1
else
# add string to result array
end
end
result
end
end
Here is the Rspec test:
describe "Array" do
describe "new_map" do
it "should not call map" do
a = [1, 2, 3]
a.stub(:map) { '' }
a.new_map { |i| i + 1 }.should eq([2, 3, 4])
end
it "should map any object" do
a = [1, "two", :three]
a.new_map { |i| i.class }.should eq([Fixnum, String, Symbol])
end
end
end
if item.is_a? Integer
result << item + 1
class Array
def new_map
result = []
self.each do |item|
yield(item)
if item.class == Integer # or if item.is_a? Integer
result << item + 1
else
# add string to result array
end
end
result
end
end
example:
=> 1.is_a? Integer
=> true
=> "1".is_a? Integer
=> false
=> 1_000_000.is_a? Integer
=> true
=> 1_000_000.class
=> Fixnum
=> 1_000_000.is_a? Integer
=> true
Try this:
class Array
def new_map
map do |item|
yield(item)
end
end
end
Actually you first spec does not make sense. You yield i + 1 to the block. This must fail if ì is not a Fixnum. You must not check if something is an Integer in your method, but in the block. This should work:
describe "Array" do
describe "new_map" do
it "should not call map" do
a = [1, 2, 3]
a.new_map { |i| (i.is_a?(Integer) ? i + 1 : i) }.should eq([2, 3, 4])
end
it "should map any object" do
a = [1, "two", :three]
a.new_map { |i| i.class }.should eq([Fixnum, String, Symbol])
end
end
end
class Array
def new_map
result = []
self.each do |item|
result << yield(item)
end
result
end
end
And both your tests pass, don't check object's class unnecessarily, trust duck typing.

How do I call a method, given its name, on an element of an array?

How do I call a method, given its name, on an element of an array?
For example, I could have:
thing = "each"
I want to be able to do something like:
def do_thing(thing)
array = [object1,object2]
array[0].thing
end
so that do_thing(to_s), for example, would run object1.to_s.
You can use public_send or send. public_send only sends to public methods while send can see public and private methods.
def do_thing(thing)
array = [1,2,3]
array.public_send(thing)
end
do_thing('first')
# => 1
do_thing(:last)
# => 3
Update A more general version:
def do_thing(array, index, method, *args)
array[index].public_send(method, *args)
end
do_thing([1, 2, 3], 0, :to_s)
# => "1"
do_thing([[1,2], [3, 4]], 0, :fetch, 0)
# => 1
require 'ostruct'
o = OpenStruct.new(attribute: 'foo')
do_thing([o], 0, :attribute=, 'bar')
o.attribute == 'bar'
# => true
Object#send
thing = "each"
def do_thing(thing)
array = [1,2,3]
array.send(thing)
end
From the doc:
class Klass
def hello(*args)
"Hello " + args.join(' ')
end
end
k = Klass.new
k.send :hello, "gentle", "readers" #=> "Hello gentle readers"
Here is an example to help you out although I don't have any idea what objects are residing inside your array:
arr = [Array.new(2,10),"abc" ]
arr.each{|i| p i.send(:length)}
#>>2
#>>3

Ruby : Rubeque - difficult with *n or n

I'm doing http://www.rubeque.com/problems/queue-continuum/solutions/51a26923ba804b00020000df and I spent a while there. I can't realize why this code doesn't pass
def initialize(queue)
#q = queue
end
def pop(n=1)
#q.shift(n)
end
def push(arr)
arr.each { |x|
#q.push(x)
}
return true
end
def to_a
#q
end
but this works perfectly.
def initialize(queue)
#q = queue
end
def pop(*n)
#q.shift(*n)
end
def push(arr)
#q.push(*arr)
return true
end
def to_a
#q
end
i'm totally confused about
def pop(*n)
#q.shift(*n)
end
and
def push(arr)
#q.push(*arr)
end
why should I take (arr) as array and than change it into... *arr which is Array of array? I'm confused, please help!
The splat works in two ways.
When receiving arguments, it combines arguments into an array.
def foo *args; args end
foo(1) # => [1]
foo(1, 2, 3) # => [1, 2, 3]
When giving arguments, it decomposes an array into arguments.
def bar x, y, z; y end
bar(*[1, 2, 3]) # => 2
def baz x; x end
baz(1) # => [1]
baz(1, 2, 3) # => Error
The *arr you are wondering is the latter case. It is not an object like [1, 2, 3] (hence, not an array of arrays). It is a part of arguments (like 1, 2, 3) passed to a method.
There are other uses of splats (as in array literals, case statements, etc.), but their function is either of the two uses above.

Resources