One-liner to change multiple strings, upcase, downcase etc? - ruby

Hey first day coding ruby and I'm wondering if there is any method to changing multiple variables in one line for instance.
first_name = "JOHN"
last_name = "DOE"
[first_name, last_name] = [first_name, last_name].downcase
Output : john doe
Thanks

To change the values, use map:
first_name = "JOHN"
last_name = "DOE"
first_name, last_name = [first_name, last_name].map &:downcase
p first_name # => john

One solution could be to use the ! version of the downcase method
[first_name, last_name].each {|str| str.downcase!}
which can also be written :
[first_name, last_name].each(&:downcase!)

You can also use "collect".
[first_name, last_name].collect(&:downcase)
which returns
=> ["john", "doe"]
and if you want to join them together as a full name, you can use "join".
[first_name, last_name].collect(&:downcase).join(' ')
which returns
=> "john doe"

Related

Is there a Ruby one-liner to join() nested Ruby arrays, with different join() string for inner/outer arrays?

For:
a = [ ["John", "Doe"], ["Sue", "Smith"]]
The desired output is: "John Doe, Sue Smith"
The brute-force code is easy:
a = [ ["John", "Doe"], ["Sue", "Smith"]]
name_array = []
a.each { |n| name_array << n.join(" ") } # first, inner join w/ space
s = name_array.join(", ") # then, outer join with comma
But is there a more succint (one-liner?) to accomplish this in Ruby?
Map and Join
You can do this with Array#map and Array#join. For example, in Ruby 2.7.1:
array = [["John", "Doe"], ["Sue", "Smith"]]
array.map { _1.join ?\s }.join ", "
#=> "John Doe, Sue Smith"
If you aren't using a recent Ruby, or find a more explicit syntax preferrable, you can do the same thing like so:
array.map { |subarray| subarray.join " " }.join ", "
#=> "John Doe, Sue Smith"
There are certainly other ways to do this, but this one-liner seems to fit your examples and use case.
a = [["John", "Doe"], ["Sue", "Smith"], ["Melba", "Jones"]]
The obvious way of doing this, that has been mentioned by others, is:
a.map { |arr| arr.join(' ') }.join(', ')
#=> "John Doe, Sue Smith, Melba Jones"
As an exercise, here are three ways this can be done without using Array#map
Use Enumerable#reduce (aka inject)
a.drop(1).reduce(a.first.join(' ')) { |s,name| s + ", %s %s" % name }
#=> "John Doe, Sue Smith, Melba Jones"
Use recursion
def doit((name, *rest))
rest.empty? ? name.join(' ') : "%s %s, %s" % [*name, doit(rest)]
end
doit(a)
#=> "John Doe, Sue Smith, Melba Jones"
Flatten, join with a space, use String#gsub to insert commas
r = /
\w+[ ]\w+ # match two words separated by a space
(?=[ ]) # positive lookahead asserts that next character is a space
\K # reset start of match to current location and discard all
# previously matched characters from match that is returned
/x # free-spacing regex definition mode
a.flatten.join(' ').gsub(r, ',')
#=> "John Doe, Sue Smith, Melba Jones"
When you want to construct a new array from an existing array (or any enumerable) really what you want to use is Enumerable#map.
Map iterates over your existing array, runs the block, and collects the results.
result = a.map { |people| people.join(' ') }.join(', ')
Broken down:
intermediate = a.map { |people| people.join(' ') }
# transforms
# [ ["John", "Doe"], ["Sue", "Smith"]]
# into
# [ "John Doe", "Sue Smith" ]
# we take the result of that and run
result = intermediate.join(', ')
# which then transforms your array in the the final string:
# "John Doe, Sue Smith"

Ruby string to hash values

I just started to learn Ruby!
I have the following string:
"Mark Smith, 29"
and I want to convert it to hash, so it looks like this:
{:name=>"Mark", :surname=>"Smith", :age=>29}
I've written the following code, to cut the input:
a1 = string.scan(/\w+|\d+/)
Now I have an array of strings. Is there an elegant way to convert this to hash? I know I can make three iterations like this:
pers = Hash.new
pers[:name] = a1[0]
pers[:surname] = a1[1]
pers[:age] = a1[2]
But maybe there is a way to do it using .each method or something like this? Or maybe it is possible to define a class Person, with predefined keys (:name, :surname, :age), and then just "throw" my string to an instance of this class?
Yes, you can do like,
%i(name surname age)
.zip(string.scan(/\w+|\d+/))
.to_h
# => {:name=>"Mark", :surname=>"Smith", :age=>"29"}
Or, you can take the benefit of Struct, like:
Person = Struct.new(:name, :surname, :age )
person = Person.new( *string.scan(/\w+|\d+/) )
person.age # => "29"
person.name # => "Mark"
For simplicity, precision and clarity I'd do it like so:
"Mark Smith, 29" =~ /(\w+)\s+(\w+),\s+(\d+)/
#=> 0
{ :name=> $1, :surname=> $2, :age=> $3.to_i }
#=> {:name=>"Mark", :surname=>"Smith", :age=>29}
Unlike /\w+|\d+/, this regex requires "Mark" and "Smith" to be strings and "29" to be digits.

Extract Hash values using Hash#dig

h = {
users: {
u_548912: {
name: "John",
age: 30
},
u_598715: {
name: "Doe",
age: 30
}
}
}
Given a hash like above, say I want to get user John, I can do
h[:users].values.first[:name] # => "John"
In Ruby 2.3 use Hash#dig can do the same thing:
h.dig(:users, :u_548912, :name) # => "John"
But given that the u_548912 is just a random number(no way to know it before hand), is there a way to get the information still using Hash#dig?
You can, of course, pass an expression as an argument to #dig:
h.dig(:users, h.dig(:users)&.keys&.first, :name)
#=> John
Extract the key if you want more legibility, at the cost of lines of code:
first_user_id = h.dig(:users)&.keys&.first
h.dig(:users, first_user_id, :name)
#=> John
Another option would be to chain your #dig method calls. This is shorter, but a bit less legible.
h.dig(:users)&.values&.dig(0, :name)
#=> John
I'm afraid there is no "neater" way of doing this while still having safe navigation.

How to do named capture in ruby

I want to name the capture of string that I get from scan. How to do it?
"555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten #=> ["555", "333", "7777"]
Is it possible to turn it into like this
{:area => "555", :city => "333", :local => "7777" }
or
[["555","area"], [...]]
I tried
"555-333-7777".scan(/((?<area>)\d{3})-(\d{3})-(\d{4})/).flatten
but it returns
[]
You should use match with named captures, not scan
m = "555-333-7777".match(/(?<area>\d{3})-(?<city>\d{3})-(?<number>\d{4})/)
m # => #<MatchData "555-333-7777" area:"555" city:"333" number:"7777">
m[:area] # => "555"
m[:city] # => "333"
If you want an actual hash, you can use something like this:
m.names.zip(m.captures).to_h # => {"area"=>"555", "city"=>"333", "number"=>"7777"}
Or this (ruby 2.4 or later)
m.named_captures # => {"area"=>"555", "city"=>"333", "number"=>"7777"}
Something like this?
"555-333-7777" =~ /^(?<area>\d+)\-(?<city>\d+)\-(?<local>\d+)$/
Hash[$~.names.collect{|x| [x.to_sym, $~[x]]}]
=> {:area=>"555", :city=>"333", :local=>"7777"}
Bonus version:
Hash[[:area, :city, :local].zip("555-333-7777".split("-"))]
=> {:area=>"555", :city=>"333", :local=>"7777"}
In case you don't really need the hash, but just local variables:
if /(?<area>\d{3})-(?<city>\d{3})-(?<number>\d{4})/ =~ "555-333-7777"
puts area
puts city
puts number
end
How does it work?
You need to use =~ regex operator.
The regex (sadly) needs to be on the left. It doesn't work if you use string =~ regex.
Otherwise it is the same syntax ?<var> as with named_captures.
It is supported in Ruby 1.9.3!
Official documentation:
When named capture groups are used with a literal regexp on the
left-hand side of an expression and the =~ operator, the captured text
is also assigned to local variables with corresponding names.
A way to turn capture group names and their values into a hash is to use a regex with named captures using (?<capture_name> and then access the %~ global "last match" variable.
regex_with_named_capture_groups = %r'(?<area>\d{3})-(?<city>\d{3})-(?<local>\d{4})'
"555-333-7777"[regex_with_named_capture_groups]
match_hash = $~.names.inject({}){|mem, capture| mem[capture] = $~[capture]; mem}
# => {"area"=>"555", "city"=>"333", "local"=>"7777"}
# If ActiveSupport is available
match_hash.symbolize_keys!
# => {area: "555", city: "333", local: "7777"}
This alternative also works:
regex = /^(?<area>\d+)\-(?<city>\d+)\-(?<local>\d+)$/
m = "555-333-7777".match regex
m.named_captures
=> {"area"=>"555", "city"=>"333", "local"=>"7777"}
There are a LOT of ways to create named captures, many of which have been mentioned already. For the record though, we could have even used the originally posted code along with Multiple Assignment like so:
a, b, c = "555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten
hash = {area: a, city: b, local: c}
#=> {:area=>"555", :city=>"333", :local=>"7777"}
OR
hash = {}
hash[:area], hash[:city], hash[:local] = "555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten
hash
#=> {:area=>"555", :city=>"333", :local=>"7777"}
OR along with zip and optionally to_h:
[:area, :city, :local].zip "555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten
#=> [[:area, "555"], [:city, "333"], [:local, "7777"]]
([:area, :city, :local].zip "555-333-7777".scan(/(\d{3})-(\d{3})-(\d{4})/).flatten).to_h
#=> {:area=>"555", :city=>"333", :local=>"7777"}

Compare multiple values in a Ruby array

[
{
"name": "John Doe",
"location": {
"name": "New York, New York",
"id": 12746342329
},
"hometown": {
"name": "Brooklyn, New York",
"id": 43453644
}
},
{
"name": "Jane Doe",
"location": {
"name": "Miami, Florida",
"id": 12746342329
},
"hometown": {
"name": "Queens, New York",
"id": 12746329
}
}
]
Given this piece of JSON, how would I be able to loop through and pull out all of the "hometown" and "location" keys and see which people had the value of New York?
My issue is I can Array.each through these items, but I don't know how to traverse both location && hometown with my criteria ("New York").
people.select {|person|
person.any? {|k, v|
%w[location hometown].include?(k) && /New York/ =~ v['name']
}}
This basically says the following: select all entries in the array for which the following condition is true. The condition is: is it true for any of the key-value pairs that the key is either 'hometown' or 'location' and the name property of the value belonging to that key matches the Regexp /New York/?
However, your object model seems to be in a serious need of refactoring. In fact, the main problem is that your object model isn't even an object model, it's a hash and array model.
Here's what I mean by a proper object model:
class Person
attr_reader :name, :location, :hometown
def initialize(name, location=nil, hometown=nil)
#name, #location, #hometown = name, location, hometown
end
def cities
return #location, #hometown
end
end
class City
attr_reader :id, :name
def initialize(id, name)
#id, #name = id, name
end
def =~(other)
name =~ other
end
end
nyc = City.new(12746342329, 'New York, New York')
brooklyn = City.new(43453644, 'Brooklyn, New York')
miami = City.new(12746342329, 'Miami, Florida')
queens = City.new(12746329, 'Queens, New York')
john = Person.new('John Doe', nyc, brooklyn)
jane = Person.new('Jane Doe', miami, queens)
people = [john, jane]
If you have such a proper object model, your code becomes much cleaner, because instead of teasing apart the nuts of bults of a nested maze of hashes and arrays, you have nice little objects that you can simply ask some questions:
people.select {|person| person.cities.any? {|city| city =~ /New York/ }}
You can almost read this like English: from the array select all people for which any of their cities matches the Regexp /New York/.
If we improve the object model further, it gets even better:
class Person
def lived_in?(pattern)
cities.any? {|city| city =~ pattern }
end
end
people.select {|person| person.lived_in?(/New York/) }
This basically says "From the people, select the ones which at one time lived in New York". That's much better than "from the people select all for which the first element of the key value pair is either the string 'hometown' or the string 'location' and the second element of the key value pair matches the Regexp /New York/".
I think Jörg's solution has a minor bug - 'location' and 'hometown' are not used, so, for example, following "person" would pass the test:
{
'name' => 'Foo Bar',
'favourite movie' => {
name => 'New York, New York!'
}
}
Here's a shot at correcting it, along with comments:
ary.select {|person| # get me every person satisfying following condition
%w[location hometown].any? {|key| # check if for any of the strings 'location' and 'hometown'
# person should have that key, and its 'name' should contain /New York/ regexp
person[key] && person[key]['name'] && /New York/ =~ person[key]['name']
}}

Resources