Performing union of two arrays with custom rules - arrays

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
...

Related

After using .flatten, identical strings aren't equal to each other

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

How to merge 2 arrays where value in one matches a value in another with different key in Ruby

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.

Turning an array into an array of hashes using .each

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} }

How to batch enumerables in ruby

In my quest to understand ruby's enumerable, I have something similar to the following
FileReader.read(very_big_file)
.lazy
.flat_map {|line| get_array_of_similar_words } # array.size is ~10
.each_slice(100) # wait for 100 items
.map{|array| process_100_items}
As much as each flat_map call emits an array of ~10 items, I was expecting the each_slice call to batch the items in 100's but that is not the case. I.e wait until there are 100 items before passing them to the final .map call.
How do I achieve functionality similar to the buffer function in reactive programming?
To see how lazy affects the calculations, let's look at an example. First construct a file:
str =<<~_
Now is the
time for all
good Ruby coders
to come to
the aid of
their bowling
team
_
fname = 't'
File.write(fname, str)
#=> 82
and specify the slice size:
slice_size = 4
Now I will read lines, one-by-one, split the lines into words, remove duplicate words and then append those words to an array. As soon as the array contains at least 4 words I will take the first four and map them into the longest word of the 4. The code to do that follows. To show how the calculations progress I will salt the code with puts statements. Note that IO::foreach without a block returns an enumerator.
IO.foreach(fname).
lazy.
tap { |o| puts "o1 = #{o}" }.
flat_map { |line|
puts "line = #{line}"
puts "line.split.uniq = #{line.split.uniq} "
line.split.uniq }.
tap { |o| puts "o2 = #{o}" }.
each_slice(slice_size).
tap { |o| puts "o3 = #{o}" }.
map { |arr|
puts "arr = #{arr}, arr.max = #{arr.max_by(&:size)}"
arr.max_by(&:size) }.
tap { |o| puts "o3 = #{o}" }.
to_a
#=> ["time", "good", "coders", "bowling", "team"]
The following is displayed:
o1 = #<Enumerator::Lazy:0x00005992b1ab6970>
o2 = #<Enumerator::Lazy:0x00005992b1ab6880>
o3 = #<Enumerator::Lazy:0x00005992b1ab6678>
o3 = #<Enumerator::Lazy:0x00005992b1ab6420>
line = Now is the
line.split.uniq = ["Now", "is", "the"]
line = time for all
line.split.uniq = ["time", "for", "all"]
arr = ["Now", "is", "the", "time"], arr.max = time
line = good Ruby coders
line.split.uniq = ["good", "Ruby", "coders"]
arr = ["for", "all", "good", "Ruby"], arr.max = good
line = to come to
line.split.uniq = ["to", "come"]
line = the aid of
line.split.uniq = ["the", "aid", "of"]
arr = ["coders", "to", "come", "the"], arr.max = coders
line = their bowling
line.split.uniq = ["their", "bowling"]
arr = ["aid", "of", "their", "bowling"], arr.max = bowling
line = team
line.split.uniq = ["team"]
arr = ["team"], arr.max = team
If the line lazy. is removed the return value is the same but the following is displayed (.to_a at the end now being superfluous):
o1 = #<Enumerator:0x00005992b1a438f8>
line = Now is the
line.split.uniq = ["Now", "is", "the"]
line = time for all
line.split.uniq = ["time", "for", "all"]
line = good Ruby coders
line.split.uniq = ["good", "Ruby", "coders"]
line = to come to
line.split.uniq = ["to", "come"]
line = the aid of
line.split.uniq = ["the", "aid", "of"]
line = their bowling
line.split.uniq = ["their", "bowling"]
line = team
line.split.uniq = ["team"]
o2 = ["Now", "is", "the", "time", "for", "all", "good", "Ruby",
"coders", "to", "come", "the", "aid", "of", "their",
"bowling", "team"]
o3 = #<Enumerator:0x00005992b1a41a08>
arr = ["Now", "is", "the", "time"], arr.max = time
arr = ["for", "all", "good", "Ruby"], arr.max = good
arr = ["coders", "to", "come", "the"], arr.max = coders
arr = ["aid", "of", "their", "bowling"], arr.max = bowling
arr = ["team"], arr.max = team
o3 = ["time", "good", "coders", "bowling", "team"]

How to convert a 2D array to a value object in ruby

I have a 2D array:
a = [["john doe", "01/03/2017", "01/04/2017", "event"], ["jane doe", "01/05/2017", "01/06/2017", "event"]...]
I would like to convert it to a value object in ruby. I found how to do it with a hash Ruby / Replace value in array of hash in the second answer of this question but not a 2D array. I would like to assign the value at a[0][0] to an attribute named "name", a[0][1] to "date1", a[0][2] to "date2" and a[0][3] to "event".
This is something like what I'd like to accomplish although it is not complete and I dont know how to assign multiple indexes to the different attributes in one loop:
class Schedule_info
arrt_accessor :name, :date1, :date2, :event
def initialize arr
#I would like this loop to contain all 4 attr assignments
arr.each {|i| instance_variable_set(:name, i[0])}
This should be short and clean enough, without unneeded metaprogramming :
data = [["john doe", "01/03/2017", "01/04/2017", "event"],
["jane doe", "01/05/2017", "01/06/2017", "event"]]
class ScheduleInfo
attr_reader :name, :date1, :date2, :type
def initialize(*params)
#name, #date1, #date2, #type = params
end
def to_s
format('%s for %s between %s and %s', type, name, date1, date2)
end
end
p info = ScheduleInfo.new('jane', '31/03/2017', '01/04/2017', 'party')
# #<ScheduleInfo:0x00000000d854a0 #name="jane", #date1="31/03/2017", #date2="01/04/2017", #type="party">
puts info.name
# "jane"
schedule_infos = data.map{ |params| ScheduleInfo.new(*params) }
puts schedule_infos
# event for john doe between 01/03/2017 and 01/04/2017
# event for jane doe between 01/05/2017 and 01/06/2017
You can't store the key value pairs in array index. Either you need to just remember that first index of array is gonna have "name" and assign a[0][0] = "foo" or just use array of hashes for the key value functionality you want to have
2.3.0 :006 > a = []
=> []
2.3.0 :007 > hash1 = {name: "hash1name", date: "hash1date", event: "hash1event"}
=> {:name=>"hash1name", :date=>"hash1date", :event=>"hash1event"}
2.3.0 :008 > a << hash1
=> [{:name=>"hash1name", :date=>"hash1date", :event=>"hash1event"}]
2.3.0 :009 > hash2 = {name: "hash2name", date: "hash2date", event: "hash2event"}
=> {:name=>"hash2name", :date=>"hash2date", :event=>"hash2event"}
2.3.0 :010 > a << hash2
=> [{:name=>"hash1name", :date=>"hash1date", :event=>"hash1event"}, {:name=>"hash2name", :date=>"hash2date", :event=>"hash2event"}]
It sounds like you want to call the attribute accessor method that corresponds to each array value. You use send to call methods programmatically. So you need an array of the method names that corresponds to the values you have in your given array. Now, assuming the class with your attributes is called Data.
attrs = [:name, :date1, :date2, :event]
result = a.map do |e|
d = Data.new
e.each.with_index do |v, i|
d.send(attrs[i], v)
end
d
end
The value result is an array of Data objects populated from your given array.
Of course, if you control the definition of your Data object, the best things would be to give it an initialize method that takes an array of values.
Try this:
class Schedule_info
arrt_accessor :name, :date1, :date2, :event
def initialize arr
#name = []
#date1 = []
#date2 = []
#event = []
arr.each |i| do
name << i[0]
date1 << i[1]
date2 << i[2]
event << i[3]
end
end
end

Resources