Good evening. I have a nested name array. Calling specific indices and using == returns true. After using .flatten , using == on the same values returns false.
name_array
=> [["Theo", "Peter Jr.", "Lucky"], ["Theo", "Peter Jr.", "Ms. K"], ["Queenie", "Andrew", "Ms. K", "Alex"], ["Queenie", "Alex"]]
irb(main):044:0> name_array[0][0]
=> "Theo"
irb(main):045:0> name_array[1][0]
=> "Theo"
irb(main):046:0> name_array[0][0] == name_array[1][0]
=> true
irb(main):047:0> name_array.flatten
=> ["Theo", "Peter Jr.", "Lucky", "Theo", "Peter Jr.", "Ms. K", "Queenie", "Andrew", "Ms. K", "Alex", "Queenie", "Alex"]
irb(main):048:0> name_array[0] == name_array[3]
=> false
The end goal here is to return an array of unique names.
My first instinct was to use .uniq but that ends up returning the original nested array. It should be noted I first got this nested array by converting a nested hash to an array then adding the array values to name_array.
irb(main):051:0> name_array.flatten
=> ["Theo", "Peter Jr.", "Lucky", "Theo", "Peter Jr.", "Ms. K", "Queenie", "Andrew", "Ms. K", "Alex", "Queenie", "Alex"]
irb(main):052:0> name_array.uniq
=> [["Theo", "Peter Jr.", "Lucky"], ["Theo", "Peter Jr.", "Ms. K"], ["Queenie", "Andrew", "Ms. K", "Alex"], ["Queenie", "Alex"]]
The main problem here is that #flatten returns a new array and not change the original one.
to solve it you have 2 ways:
Define a new variable with an array that returns after the call of #flatten
flatten_array = name_array.flatten
flatten_array[0] == flatten_array[3]
=> true
Use #flatten!, that change original array:
name_array.flatte!
name_array[0] == name_array[3]
the same with #uniq, to change original use: #uniq!
Summary: to solve what you need:
name_array.flatten!
name_array.uniq # or .uniq!
# OR
flatten_array = name_array.flatten
flatten_array.uniq # or .uniq!
and one more trick, using: #inject and #|
name_array.inject(&:|) # will do the same as name_array.flatten.uniq
P.S. it's second your question with the very same problem, I think this might help you to understand how it works ":)
In general: methods with an exclamation mark(!) will change your original variable value, methods without an exclamation mark(!) will return you a new value
Related
I have an array that contains other arrays of items with prices but when one has a sale a new item is created How do I merge or pull value from one to the other to make 1 array so that the sale price replaces the non sale but contains the original price?
Example:
items=[{"id": 123, "price": 100, "sale": false},{"id":456,"price":25,"sale":false},{"id":678, "price":75, "sale":true, "parent_price_id":123}]
Transform into:
items=[{"id":456,"price":25,"sale":false},{"id":678, "price":75, "sale":true, "parent_price_id":123, "original_price": 100}]
It's not the prettiest solution, but here's one way you can do it. I added a minitest spec to check it against the values you provided and it gives the answer you're hoping for.
require "minitest/autorun"
def merge_prices(prices)
# Create a hash that maps the ID to the values
price_map =
prices
.map do |price|
[price[:id], price]
end
.to_h
# Create a result array which is initially duplicated from the original
result = prices.dup
result.each do |price|
if price.key?(:parent_price)
price[:original_price] = price_map[price[:parent_price]][:price]
# Delete the original
result.delete_if { |x| x[:id] == price[:parent_price] }
end
end
result
end
describe "Merge prices" do
it "should work" do
input = [
{"id":123, "price": 100, "sale": false},
{"id":456,"price":25,"sale": false},
{"id":678, "price":75, "sale": true, "parent_price":123}
].freeze
expected_output = [
{"id":456,"price":25,"sale": false},
{"id":678, "price":75, "sale": true, "parent_price":123, "original_price": 100}
].freeze
assert_equal(merge_prices(input), expected_output)
end
end
Let's being by defining items in an equivalent, but more familiar, way:
items = [
[{:id=>123, :price=>100, :sale=>false}],
[{:id=>456, :price=>25, :sale=>false}],
[{:id=>678, :price=>75, :sale=>true, :parent_price=>123}]
]
with the desired return value being:
[
{:id=>456, :price=>25, :sale=>false},
{:id=>678, :price=>75, :sale=>true, :parent_price=>123,
:original_price=>100}
]
I assume that h[:sale] #=> false for every element of items (a hash) g for which g[:parent_price] = h[:id].
A convenient first step is to create the following hash.
h = items.map { |(h)| [h[:id], h] }.to_h
#=> {123=>{:id=>123, :price=>100, :sale=>false},
# 456=>{:id=>456, :price=>25, :sale=>false},
# 678=>{:id=>678, :price=>75, :sale=>true, :parent_price=>123}}
Then:
h.keys.each { |k| h[k][:original_price] =
h.delete(h[k][:parent_price])[:price] if h[k][:sale] }
#=> [123, 456, 678] (not used)
h #=> {456=>{:id=>456, :price=>25, :sale=>false},
# 678=>{:id=>678, :price=>75, :sale=>true, :parent_price=>123,
# :original_price=>100}}
Notice that Hash#delete returns the value of the deleted key.
The last two steps are to extract the values from this hash and replace items with the resulting array of hashes:
items.replace(h.values)
#=> [{:id=>456, :price=>25, :sale=>false},
# {:id=>678, :price=>75, :sale=>true, :parent_price=>123,
# :original_price=>100}]
See Array#replace.
If desired we could combine these steps as follows.
items.replace(
items.map { |(h)| [h[:id], h] }.to_h.tap do |h|
h.keys.each { |k| h[k][:original_price] =
h.delete(h[k][:parent_price])[:price] if h[k][:sale] }
end.values)
#=> [{:id=>456, :price=>25, :sale=>false},
# {:id=>678, :price=>75, :sale=>true, :parent_price=>123,
# :original_price=>100}]
See Object#tap.
I have a Python list:
test_list = ['LeBron James', 'and', 'Nina Mata', 'Dan Brown', 'Derrick Barnes', 'and',
'Gordon C. James', 'J. Kenji López-Alt', 'and', 'Gianna Ruggiero', ]
I want output like this:
final_list = ['LeBron James and Nina Mata', 'Dan Brown', 'Derrick Barnes and
Gordon C. James', 'J. Kenji López-Alt and Gianna Ruggiero']
In short, I want one item before 'and' one item after 'and' to be combined. On the other hand, names coming without 'and' should not be combined and left as it is. How can we do this in Python?
Here's a solution perhaps not too elegant but simple and functional: join all words with a glue symbol that does not happen in any of them (e.g., "~"), replace the resulting "~and~"s with " and "s, and split by the glue symbol again:
"~".join(test_list).replace("~and~", " and ").split("~")
#['LeBron James and Nina Mata', 'Dan Brown',
# 'Derrick Barnes and Gordon C. James',
# 'J. Kenji López-Alt and Gianna Ruggiero']
This should work for groups with more than one "and," too.
Given this array of String names, return an array of Hashes. Each Hash should have the keys name and id, which will represent their unique identifier in the form of an integer. The ids can start at 1 and then go up by one.
output should look like:
[{:name=>"Bruno", :id=>1},
{:name=>"Bella", :id=>2},
{:name=>"Ringo", :id=>3},
{:name=>"Spot", :id=>4},
{:name=>"Fluffy", :id=>5},
{:name=>"Snowball", :id=>6},
{:name=>"Doc", :id=>7}]
I'm trying to do something like this but I'm having trouble coming up with the right syntax and methods.
NAMES = ['Bella', 'Bruno', 'Ringo', 'Spot', 'Fluffy', 'Snowball', 'Doc']
def array_to_hash_array(names)
i = 1
NAMES.each do |name|
name = Hash.new(:name => name, :id => i)
NAMES << name
i += 1
end
return NAMES
end
puts array_to_hash_array(NAMES)
You can use map and with_index to return an array of Hash objects with the name and id values:
names = ['Bella', 'Bruno', 'Ringo', 'Spot', 'Fluffy', 'Snowball', 'Doc']
def array_to_hash_array(array)
array.map.with_index(1) do |name, index|
{ id: index, name: name }
end
end
p array_to_hash_array(names)
# => [{:id=>1, :name=>"Bella"}, {:id=>2, :name=>"Bruno"}, {:id=>3, :name=>"Ringo"}, {:id=>4, :name=>"Spot"}, {:id=>5, :name=>"Fluffy"}, {:id=>6, :name=>"Snowball"}, {:id=>7, :name=>"Doc"}]
I've reworked the code in your example to produce the correct output (with some notes on what was tweaked):
# variable renamed from NAMES as uppercase is meant for constants
names = ['Bella', 'Bruno', 'Ringo', 'Spot', 'Fluffy', 'Snowball', 'Doc']
def array_to_hash_array(array)
# added a result array for building up the final object to be returned
result = []
i = 1
# changed to iterate over the passed in array object
array.each do |name|
# using Hash literal instead of Hash.new since it allows us
# to specify contents of hash object directly
name = { :name => name, :id => i }
# push to result object instead of same iterated array
result << name
i += 1
end
# return built up result object
result
end
p array_to_hash_array(names)
# => [{:name=>"Bella", :id=>1}, {:name=>"Bruno", :id=>2}, {:name=>"Ringo", :id=>3}, {:name=>"Spot", :id=>4}, {:name=>"Fluffy", :id=>5}, {:name=>"Snowball", :id=>6}, {:name=>"Doc", :id=>7}]
Indexing an Array with #each_with_index
As Array includes the Enumerable module, you can invoke Array#each_with_index to create the data structure you want using the array index of each element of your names array with a suitable offset. For example:
names = %w[Bella Bruno Ringo Spot Fluffy Snowball Doc]
array = []
names.each_with_index do |name, index|
array << { name: name, id: index.succ }
end
Note the use of Integer#succ, which is used to offset the index by one. Since Ruby arrays are zero-indexed, you need to increment the index value passed in by #each_with_index to get the numbering the way you want.
You can pretty-print the array with Kernel#pp (e.g. pp array) to verify the results:
[{:name=>"Bella", :id=>1},
{:name=>"Bruno", :id=>2},
{:name=>"Ringo", :id=>3},
{:name=>"Spot", :id=>4},
{:name=>"Fluffy", :id=>5},
{:name=>"Snowball", :id=>6},
{:name=>"Doc", :id=>7}]
Note on Hash Key Ordering
As you can see, this populated the array of hashes the way you wanted, and in the specific format you're looking for. However, it's important to remember that while the order of keys in a hash are kept in insertion order in mainline Ruby, you shouldn't really rely on that behavior. Conceptually, hashes are unordered, so:
[{:id=>1, :name=>"Bella"},
{:id=>2, :name=>"Bruno"},
{:id=>3, :name=>"Ringo"},
{:id=>4, :name=>"Spot"},
{:id=>5, :name=>"Fluffy"},
{:id=>6, :name=>"Snowball"},
{:id=>7, :name=>"Doc"}] ==
[{:name=>"Bella", :id=>1},
{:name=>"Bruno", :id=>2},
{:name=>"Ringo", :id=>3},
{:name=>"Spot", :id=>4},
{:name=>"Fluffy", :id=>5},
{:name=>"Snowball", :id=>6},
{:name=>"Doc", :id=>7}]
#=> true
You can try it
names.each_with_index.map { |x,i| {name: x, id: i+1} }
names = ['Bella', 'Bruno', 'Ringo', 'Spot', 'Fluffy', 'Snowball', 'Doc']
enum = 1.step
#=> (1.step)
names.map { |name| { name: name, id: enum.next } }
#=> [{:name=>"Bella", :id=>1}, {:name=>"Bruno", :id=>2},
# {:name=>"Ringo", :id=>3}, {:name=>"Spot", :id=>4},
# {:name=>"Fluffy", :id=>5}, {:name=>"Snowball", :id=>6},
# {:name=>"Doc", :id=>7}]
See Numeric#step and Enumerator#next.
You could zip (Enumerable#zip) to an endless range (beginless-endless-range) then map to the desired hash:
ary = ['Bella', 'Bruno', 'Ringo', 'Spot', 'Fluffy', 'Snowball', 'Doc']
ary.zip(1..).map { |name, id| {name: name, id: id} }
I am trying to figure out the following:
When I run this in the terminal using Ruby, the string in the array is removed until it is done when I continue typing in a string that is in the saxophone_section array. But I still want to be able to remove the string from the array when I type in "alto saxophone 1" and because "alto 1" is found in the input string.
How can I do this when a string in an array matches, regardless of the size of an input string?
saxophone_section = ["alto 1", "alto 2", "tenor 1", "tenor 2", "bari sax"]
until saxophone_section == []
puts "Think of sax parts in a jazz big band."
print ">>"
sax_part = gets.chomp.downcase
# this is the part that is confusing me. Trying to figure out the method in which
# a string in the above array matches an input, whether "alto 1" or "alto saxophone 1"
# or "Eb alto saxophone 1" is typed in ("alto 1" is found in all).
# How can I make it true in all three (or more) cases?
saxophone_section.any?(sax_part)
# I am thinking that this bottom parts one could be used? or not?
parts = saxophone_section.map {|sax| sax.gsub(/\s+/m, ' ').strip.split(" ")}
#this is the loop to delete the item in the array:
if saxophone_section.include?(sax_part) == true
p saxophone_section.delete_if{ |s| s == sax_part}
puts "Damn, you're lucky"
else
puts "Woops! Try again."
end
end
puts "You got all parts."
Converting strings into array and making an intersection operation should be an option. I know, this is not the best solution, but might save your day.
[17] pry(main)> x = "alto saxophone 1"
=> "alto saxophone 1"
[18] pry(main)> y = "i am not an anglophone"
=> "i am not an anglophone"
[19] pry(main)> z = "alto 1"
=> "alto 1"
[20] pry(main)> x.split & z.split == z.split # & is array intersection
=> true
[21] pry(main)> x.split & y.split == y.split
=> false
You should use regex to match the input. So instead of creating an array of strings, try the array of regular expressions like so;
saxophone_section = [/alto\s(?:.*\s)?1/, /alto\s(?:.*\s)?2/, /tenor\s(?:.*\s)?1/, /tenor\s(?:.*\s)?2/, /bari\s(?:.*\s)?sax/]
Then use match with all the elements in the array against the input to find if there is a match with the input string;
sax_part = gets.chomp.downcase
index = saxophone_section.find_index { |regex| sax_part.match(regex) }
Later you can use this index to remove the element from array if it's not nil;
saxophone_section.delete(index)
Or you can just use Array#delete_if method to delete the element from array directly like so;
saxophone_section.delete_if { |regex| sax_part.match(regex) }
Note: You can use https://www.rubular.com to test your regular expressions.
Here's where I'd start with this sort of task; These are great building blocks for human-interfaces on the web or in applications:
require 'regexp_trie'
saxophone_section = ["alto 1", "alto 2", "tenor 1", "tenor 2", "bari sax"]
RegexpTrie.union saxophone_section # => /(?:alto\ [12]|tenor\ [12]|bari\ sax)/
The output of RegexpTrie.union is a pattern that will match all of the strings in saxophone_section. The pattern is concise and efficient, and best of all, doesn't have to be generated by hand.
Applying that pattern to the string being created will show if you have a hit when there's a match, but only when there's enough of the string to match.
That's where a regular Trie is very useful. When you're trying to find what possible hits you could have, prior to having a full match, a Trie can find all the possibilities:
require 'trie'
trie = Trie.new
saxophone_section = ["alto 1", "alto 2", "tenor 1", "tenor 2", "bari sax"]
saxophone_section.each { |w| trie.add(w) }
trie.children('a') # => ["alto 1", "alto 2"]
trie.children('alto') # => ["alto 1", "alto 2"]
trie.children('alto 2') # => ["alto 2"]
trie.children('bari') # => ["bari sax"]
Blend those together and see what you come up with.
I have two arrays
b = ["John Roberts", "William Koleva", "Lili Joe", "Victoria Jane", "Allen Thomas"]
a = ["Jon Roberts", "Wil Koleva", "Lilian Joe", "Vic Jane", "Al Thomas"]
Currently I am using the union operator on these two arrays, like this: a | b. When combined, even though the names in each array are the "same" name (they're just using the shortened version of the name), it will duplicate my names.
My proposed solution to this is simply choose the first occurrence of first initial + last name as the name to perform the union on, however, I don't recall there being any methods in Ruby that can perform such an operation.
So the result of some_method(a | b) will return c which is just:
["John Roberts", "William Koleva", "Lili Joe", "Victoria Jane", "Allen Thomas"]
I am wondering how I could go about achieving this?
b = ["John Roberts", "William Koleva", "Lili Joe", "Victoria Jane", "Allen Thomas"]
a = ["Jon Roberts", "Wil Koleva", "Lilian Joe", "Vic Jane", "Al Thomas"]
r = /
\s # match a space
[[:alpha:]]+ # match > 0 alphabetic characters
\z # match end of string
/x # free-spacing regex definition mode
(b+a).uniq { |str| [str[0], str[r]] }
#=> ["John Roberts", "William Koleva", "Lili Joe", "Victoria Jane", "Allen Thomas"]
This uses the form of the method Array#uniq that employs a block.
You may alternatively write (b|a).uniq { |str| [str[0], str[r]] }
The steps are as follows.
c = b+a
# => ["John Roberts", "William Koleva", "Lili Joe", "Victoria Jane", "Allen Thomas",
# "Jon Roberts", "Wil Koleva", "Lilian Joe", "Vic Jane", "Al Thomas"]
The first element of c passed to the block is
str = c.first
#=> "John Roberts"
so the block calculation is
[str[0], str[r]]
#=> ["J", " Roberts"]
The calculations are similar for all the other elements of c. The upshot is that
c.uniq { |str| [str[0], str[r]] }
is equivalent to selecting the first elements of c, when converted to [<first name initial>, <last name>], that match an element of the array d, where
d = [["J", "Roberts"], ["W", "Koleva"], ["L", "Joe"], ["V", "Jane"], ["A", "Thomas"],
["J", "Roberts"], ["W", "Koleva"], ["L", "Joe"], ["V", "Jane"], ["A", "Thomas"]].uniq
#=> [["J", "Roberts"], ["W", "Koleva"], ["L", "Joe"], ["V", "Jane"], ["A", "Thomas"]]
Pascal suggested that it would be better for uniq's block to return a string:
{ |str| "#{str[0]} #{str[r]}" }
(e.g., "J Roberts") which might instead be written
{ |str| str.sub(/(?<=.)\S+/,"") }
The inclusion of the space after the first initial is optional (e.g., "JRoberts" would also work).
Sure, just use Enumerable#uniq with a block:
c = (a | b).uniq do |full_name|
first_name, last_name = full_name.split(nil, 2)
[first_name[0], last_name]
end
Note: the first iteration of the code used the initials instead of abbreviated name.
Perhaps you can introduce the concept of a Name? It's a bit more code than just providing a block to uniq but it nicely encapsulates everything related.
class Name
def initialize(first, last)
#first, #last = first, last
end
def abbreviated
"#{#first[0]} #{#last}"
end
def eql?(other)
return false if !other.respond_to?(:abbreviated)
abbreviated == other.abbreviated
end
def hash
abbreviated.hash
end
def full
"#{#first} #{#last}"
end
end
a = Name.new('John', 'Roberts')
b = Name.new('Jon', 'Roberts')
c = Name.new('William', 'Koleva')
d = Name.new('Wil', 'Koleva')
x = [a, c]
y = [b, d]
p (y | x).map(&:full)
It's worth noting that abbreviated firstname does not really suffice to check equality of names.
Consider:
Jim Jackson
James Jackson
Janine Jackson
...