Delete duplicate elements from Array - arrays

I have two arrays, array1 and array2, as follows:
array1 = [ obj11, obj21, obj31 ]
array2 = [ obj21, obj22, obj23 ]
Objects in both arrays are from the same class. I want to check if array1 contains objects that already exist in array2 and delete them.
Let's say obj11 and obj22 are equal. By "equal," I mean they have similar attribute values. Then I would like to delete obj11 from array1, then insert obj21 and obj31 in array2.
I already define equality for attributes in the objects' class from here:
def ==(other)
return self.a == other.a && self.b == other.b
end
The resulting array would be:
array2 = [ obj21, obj22, obj23, obj21, obj31 ]

You can use Array#| ( it does union operation) to remove duplicates too.
array1 = ["dog", "cat", "had"]
array2 = ["big", "fight", "had"]
array1 | array2
# => ["dog", "cat", "had", "big", "fight"]

A quick way to remove duplicate values from multiple arrays is by using uniq
array1 = ["dog", "cat", "had"]
array2 = ["big", "fight", "had"]
new_array = (array1 + array2).uniq # <- ["dog", "cat", "had", "big", "fight"]
uniq removes duplicate values from an array. By combining array1 and array2 together, you can then filter out duplicates between the two of them.

If I want to solve your problem literally, then, I will write something like this:
array1 = [ :obj11, :obj21, :obj31 ]
array2 = [ :obj21, :obj22, :obj23 ]
new_array = (array1 - array2) + array2
p new_array
(array1 - array2) takes those elements from array1 that are also present in array2 and adding array2 to that will get you the final result
Output
[:obj11, :obj31, :obj21, :obj22, :obj23]
(Note: I used symbols as elements of arrays for illustration purposes)

I got the answer. In the following I delete from array1 what is already exist in array2. Equality here works as I define it in the question. Thus, checking if attributes (that have been defined in the method ==) are equal.
array1.delete_if{|elem| array2.any?{|e| e == elem}}
Then add the rest of array1 into array2.
array2 << array1
Then I flatten array2.
array2.flatten!

You could do that as follows:
a2.concat(a1.delete_if { |e| a2.include?(e) })
Here's an example:
class MyClass
attr_reader :a, :b, :c
def initialize(a, b, c)
#a, #b, #c = a, b, c
end
def ==(other)
self.a == other.a && self.b == other.b
end
end
a1 = [MyClass.new('cat', 'bat', 'rat'),
MyClass.new('dog', 'hog', 'pig'),
MyClass.new('jay', 'bee', 'fly'),]
#=> [#<MyClass:0x007fca8407b678 #a="cat", #b="bat", #c="rat">,
# #<MyClass:0x007fca8407bee8 #a="dog", #b="hog", #c="pig">,
# #<MyClass:0x007fca84073ef0 #a="jay", #b="bee", #c="fly">]
a2 = [MyClass.new('fly', 'bee', 'bat'),
MyClass.new('cat', 'bat', 'rat'),
MyClass.new('dog', 'hog', 'cat'),]
#=> [#<MyClass:0x007fca840382d8 #a="fly", #b="bee", #c="bat">,
# #<MyClass:0x007fca840381e8 #a="cat", #b="bat", #c="rat">,
# #<MyClass:0x007fca840380d0 #a="dog", #b="hog", #c="cat">]
a2.concat(a1.delete_if { |e| a2.include?(e) })
#=> [#<MyClass:0x007f96d404ea08 #a="fly", #b="bee", #c="bat">,
# #<MyClass:0x007f96d404e8c8 #a="cat", #b="bat", #c="rat">,
# #<MyClass:0x007f96d404e710 #a="dog", #b="hog", #c="cat">,
# #<MyClass:0x007f96d409d9c8 #a="jay", #b="bee", #c="fly">]
a1
#=> [#<MyClass:0x007f96d409d9c8 #a="jay", #b="bee", #c="fly">]
If we change the first element of a1 from:
MyClass.new('cat', 'bat', 'rat')
to:
MyClass.new('cat', 'rat', 'bat')
we obtain:
a2.concat(a1.delete_if { |e| a2.include?(e) })
#=> [#<MyClass:0x007f89528568c0 #a="fly", #b="bee", #c="bat">,
# #<MyClass:0x007f8952856708 #a="cat", #b="bat", #c="rat">,
# #<MyClass:0x007f89528562d0 #a="dog", #b="hog", #c="cat">,
# #<MyClass:0x007f89519277f0 #a="cat", #b="rat", #c="bat">,
# #<MyClass:0x007f8951927598 #a="jay", #b="bee", #c="fly">]

Another way (using intersection operation):
array1 = [ 1, 2, 3, 4, 5 ]
array2 = [ 2, 3, 4, 5, 6 ]
final_array = array1 + array2
final_array & final_array
This will also delete duplicates. IRB output:
2.2.1 :012 > array1 = [ 1, 2, 3, 4, 5 ]
=> [1, 2, 3, 4, 5]
2.2.1 :013 > array2 = [ 2, 3, 4, 5, 6 ]
=> [2, 3, 4, 5, 6]
2.2.1 :014 > final_array = array1 + array2
=> [1, 2, 3, 4, 5, 2, 3, 4, 5, 6]
2.2.1 :015 > final_array & final_array
=> [1, 2, 3, 4, 5, 6]

Related

How do I group and add values from nested hashes and arrays with same key?

I am trying to get the sum of points and average grade for each student inside this combination of hashes and arrays but all my attempts only return the general sum for all entries. Any ideas?
student_data =
{"ST4"=>[{:student_id=>"ST4", :points=> 5, :grade=>5},
{:student_id=>"ST4", :points=>10, :grade=>4},
{:student_id=>"ST4", :points=>20, :grade=>5}],
"ST1"=>[{:student_id=>"ST1", :points=>10, :grade=>3},
{:student_id=>"ST1", :points=>30, :grade=>4},
{:student_id=>"ST1", :points=>45, :grade=>2}],
"ST2"=>[{:student_id=>"ST2", :points=>25, :grade=>5},
{:student_id=>"ST2", :points=>15, :grade=>1},
{:student_id=>"ST2", :points=>35, :grade=>3}],
"ST3"=>[{:student_id=>"ST3", :points=> 5, :grade=>5},
{:student_id=>"ST3", :points=>50, :grade=>2}]}
The desired hash can be obtained thusly.
student_data.transform_values do |arr|
points, grades = arr.map { |h| h.values_at(:points, :grade) }.transpose
{ :points=>points.sum, :grades=>grades.sum.fdiv(grades.size) }
end
#=> {"ST4"=>{:points=>35, :grades=>4.666666666666667},
# "ST1"=>{:points=>85, :grades=>3.0},
# "ST2"=>{:points=>75, :grades=>3.0},
# "ST3"=>{:points=>55, :grades=>3.5}}
The first value passed to the block is the value of the first key, 'ST4' and the block variable arr is assigned that value:
a = student_data.first
#=> ["ST4",
# [{:student_id=>"ST4", :points=> 5, :grade=>5},
# {:student_id=>"ST4", :points=>10, :grade=>4},
# {:student_id=>"ST4", :points=>20, :grade=>5}]
# ]
arr = a.last
#=> [{:student_id=>"ST4", :points=> 5, :grade=>5},
# {:student_id=>"ST4", :points=>10, :grade=>4},
# {:student_id=>"ST4", :points=>20, :grade=>5}]
The block calculations are as follows. The first value of arr passed by map to the inner block is
h = arr.first
#=> {:student_id=>"ST4", :points=>5, :grade=>5}
h.values_at(:points, :grade)
#=> [5, 5]
After the remaining two elements of arr are passed to the block we have
b = arr.map { |h| h.values_at(:points, :grade) }
#=> [[5, 5], [10, 4], [20, 5]]
Then
points, grades = b.transpose
#=> [[5, 10, 20], [5, 4, 5]]
points
#=> [5, 10, 20]
grades
#=> [5, 4, 5]
We now simply form the hash that is the value of 'ST4'.
c = points.sum
#=> 35
d = grades.sum
#=> 14
e = grades.size
#=> 3
f = c.fdiv(d)
#=> 4.666666666666667
The value of 'ST4' in student_data therefore maps to the hash
{ :points=>c, :grades=>f }
#=> {:points=>35, :grades=>4.666666666666667}
The mappings of the remaining keys of student_data are computed similarly.
See Hash#transform_values, Enumerable#map, Hash#values_at, Array#transpose, Array#sum and Integer#fdiv.
Whatever you expect can be achieved as below,
student_data.values.map do |z|
z.group_by { |x| x[:student_id] }.transform_values do |v|
{
points: v.map { |x| x[:points] }.sum, # sum of points
grade: (v.map { |x| x[:grade] }.sum/v.count.to_f).round(2) # average of grades
}
end
end
As exact expected output format is not specified, obtained in following way,
=> [
{"ST4"=>{:points=>35, :grade=>4.67}},
{"ST1"=>{:points=>85, :grade=>3.0}},
{"ST2"=>{:points=>75, :grade=>3.0}},
{"ST3"=>{:points=>55, :grade=>3.5}}
]
For Ruby 2.6 using Object#then or Object#yield_self for Ruby 2.5
student_data.transform_values { |st| st
.each_with_object(Hash.new(0)) { |h, hh| hh[:sum_points] += h[:points]; hh[:sum_grade] += h[:grade]; hh[:count] += 1.0 }
.then{ |hh| {tot_points: hh[:sum_points], avg_grade: hh[:sum_grade]/hh[:count] } }
}
How it works?
Given the array for each student:
st = [{:student_id=>"ST4", :points=> 5, :grade=>5}, {:student_id=>"ST4", :points=>10, :grade=>4}, {:student_id=>"ST4", :points=>20, :grade=>5}]
First build a hash adding and counting using Enumerable#each_with_object with a Hash#default set at zero (Hash.new(0))
step1 = st.each_with_object(Hash.new(0)) { |h, hh| hh[:sum_points] += h[:points]; hh[:sum_grade] += h[:grade]; hh[:count] += 1.0 }
#=> {:sum_points=>35, :sum_grade=>14, :count=>3.0}
Then use then! (yield_self for Ruby 2.5)
step2 = step1.then{ |hh| {tot_points: hh[:sum_points], avg_grade: hh[:sum_grade]/hh[:count] }}
#=> {:tot_points=>35, :avg_grade=>4.666666666666667}
Put all together using Hash#transform_values as in the first snippet of code

Ruby: insert and sort numbers

I have to create a method in Ruby which inserts a number and sorts the resulting list.
Input would be like:
insert_number([2.0,3.5,4.8], 4.1)
which should output:
[2.0,3.5,4.1,4.8]
With input like:
insert_number([], 5.1)
it should output:
[5.1]
Here is my incomplete code:
def insert_number(list, number)
new_list = []
position = 0
number_has_been_inserted = false # Remember whether a new number
# has been inserted.
while position < list.length
position += 1
new_list = list + [number]
...
end
...
new_list
end
print insert_number([2.0,3.5,4.8], 4.1)
bsearch only works if the original input array is already sorted, which is not a pre-condition. – #pjs
Considering your original array is sorted you can use binary search here. It will perform much better because it won't need to perform expensive sorting procedure on each insert.
This one mutates original array
def insert_number(arr, num)
i = (0...arr.size).bsearch{ |a| arr[a] > num }
i ||= arr.size
arr.insert(i, num)
end
arr = []
insert_number(arr, 1)
#=> [1]
insert_number(arr, 2)
# => [1, 2]
insert_number(arr, 2.1)
# => [1, 2, 2.1]
insert_number(arr, 1.3)
#=> [1, 1.3, 2, 2.1]
And this one will return new array on each call
def insert_number(arr, num)
i = (0...arr.size).bsearch{ |a| arr[a] > num }
i ||= arr.size
arr[0, i] + [num] + arr[i..-1]
# or
# arr.dup.insert(i, num)
end
arr = []
arr = insert_number(arr, 1)
#=> [1]
arr = insert_number(arr, 2)
# => [1, 2]
arr = insert_number(arr, 2.1)
# => [1, 2, 2.1]
arr = insert_number(arr, 1.3)
#=> [1, 1.3, 2, 2.1]
PS:
Recent Ruby versions have bsearch_index – #Stefan
I'd do something like:
def insert_number(list, number)
(list << number).sort
end
list = [1, 2, 3]
insert_number(list, 2.1) # => [1, 2, 2.1, 3]
insert_number(list, 4.1) # => [1, 2, 2.1, 3, 4.1]
insert_number([], 1) # => [1]
The problem is this changes list. If that's not desired then use dup:
def insert_number(list, number)
(list.dup << number).sort
end
list = [1, 2, 3]
insert_number(list, 2.1) # => [1, 2, 2.1, 3]
insert_number(list, 4.1) # => [1, 2, 3, 4.1]
insert_number([], 1) # => [1]
or a "splat" AKA *:
def insert_number(list, number)
[*list, number].sort
end
list = [1, 2, 3]
insert_number(list, 2.1) # => [1, 2, 2.1, 3]
insert_number(list, 4.1) # => [1, 2, 3, 4.1]
insert_number([], 1) # => [1]
[*list, number] tells Ruby to explode the array list into its elements, effectively creating a new array:
[1, 2, 3, number]
A little shorter without having to use dup.
def insert_number(list, number)
(list + [number]).sort
end
use Array#sort:
def insert_number(list,number=false)
number.is_a?(Numeric) ? list.push(number).sort : list.sort
end
The above method uses the ternary operator which is a short form of an if-statement.
Use In-Place Array Methods
In-place operations on the array should be fastest, since they won't have to create additional arrays. For example:
#array = [2.0, 3.5, 4.8]
def insert_number float
(#array << float).sort!
end
If you can't operate directly on a shared variable, or want to reduce coupling, then you can still shave some time with the in-place sort rather than returning a new sorted array:
def insert_number array, float
(array.dup << float).sort!
end
In all cases, appending to an array with << should be faster than creating new arrays with the + method.
Actually we do not need any sort because we know that our array is sorted, and then when we will new number x we can put it into array with O(n) time, where n is size of array. We will go one by one, let's take i as index and our new array will be built like NewArray = (left numbers < x) + x + (x < right number): here left numbers are that numbers less than x and right number are numbers bigger than x.
ispushed = false
arr.each do |number|
if number < x
NewArray.push(number)
elsif ispushed == false
ispushed=true
NewArray.push(x)
newArray.push(number)
else
NewArray.push(number)
if our x is biggest so that ispushed will be remained false
in the end we can just push our 'x' number
if you use sort it works by O(nlogn) time complexity

Multiple return values with arrays and hashes

I believe arrays are mostly used for returning multiple values from methods:
def some_method
return [1, 2]
end
[a, b] = some_method # should yield a = 1 and b = 2
I presume this is a kind of syntactic sugar that Ruby provides. Can we get a similar result with hashes, for instance
def some_method
return { "a" => 1, "b" => 2 }
end
{"c", "d"} = some_method() # "c" => 1, "d" => 2
I'm looking for the result { "c" => 1, "d" => 2 }, which obviously does not happen. Is there any other way this can be done? I know that we can return a hash from the method and store it and use it like so
def some_method
return {"a" => 1, "b" => 2}
end
hash = some_method()
Just curious if there is another way similar to the one with arrays but using hashes....
I think a simpler way to put the question would be...
# If we have a hash
hash = {"a" => 1, "b" => 2}
# Is the following possible
hash = {2, 3} # directly assigning values to the hash.
OR
# another example
{"c", "d"} = {2, 3} # c and d would be treated as keys and {2, 3} as respective values.
First of all, you have a syntax error. Instead of this:
[a, b] = [1, 2]
you should use:
a, b = [1, 2]
And if you want to use similar syntax with hashes, you can do:
a, b = { "c" => 1, "d" => 2 }.values # a => 1, b => 2
This is actually the same thing as the array version, beacause Hash#values returns an array of the hash values in the order they were inserted to the hash (because ruby hashes have a nice feature of preserving their order)
What you are asking is syntactically not possible.
What you want to accomplish is possible, but you will have to code it.
One possible way to do that is shown below
hash = {"a" => 1, "b" => 2}
# Assign new keys
new_keys = ["c", "d"]
p [new_keys, hash.values].transpose.to_h
#=> {"c"=>1, "d"=>2}
# Assign new values
new_values = [3, 4]
p [hash.keys, new_values].transpose.to_h
#=> {"a"=>3, "b"=>4}
If you really want some more easier looking way of doing, you could monkey-patch Hash class and define new methods to manipulate the values of keys and values array. Please be cautioned that it may not be really worthwhile to mess with core classes. Anyways, a possible implementation is shown below. Use at your own RISK.
class Hash
def values= new_values
new_values.each_with_index do |v, i|
self[keys[i]] = v if i < self.size
end
end
def keys= new_keys
orig_keys = keys.dup
new_keys.each_with_index do |k, i|
if i < orig_keys.size
v = delete(orig_keys[i])
self[k] = v
rehash
end
end
end
end
hash = {"a" => 1, "b" => 2}
hash.values = [2,3]
p hash
#=> {"a"=>2, "b"=>3}
hash.keys = ["c", "d"]
p hash
#=> {"c"=>2, "d"=>3}
hash.keys, hash.values = ["x","y"], [9, 10]
p hash
#=> {"x"=>9, "y"=>10}
hash.keys, hash.values = ["x","y"], [9, 10]
p hash
#=> {"x"=>9, "y"=>10}
# *** results can be unpredictable at times ***
hash.keys, hash.values = ["a"], [20, 10]
p hash
#=> {"y"=>20, "a"=>10}

Destructive reject from an array returning the values rejected

Is there a sensible way to do the following:
I want to take an array and select specific items from the array according to conditions, removing them from the array as they go.
(I basically want to split the contents of an array into categories).
array = [1,2,3,4,5,6,7,8]
less_than_three = array.reject_destructively{|v| v<3}
=> [1,2]
array
=> [3,4,5,6,7,8]
more_than_five = array.reject_destructively{|v| v>5}
=> [6,7,8]
array
=> [3,4,5]
I've tried delete_if, select!, reject! and none of them seem to be able to give you the affected items whilst leaving the array with the rest.
Unless I'm going mad, which is entirely possible.
As I understood the question, you do not want to produce two new objects. Here you go:
class Array
def carve!
dup.tap { delete_if &Proc.new } - self
end
end
array = [1,2,3,4,5,6,7,8]
p array.carve! { |v| v < 3 }
#⇒ [1, 2] # returned by Array#carve method
p array
#⇒ [3, 4, 5, 6, 7, 8] # remained in original array
Using this solution, array.__id__ remains the same. And this is the golfiest answer all around :)
You can build your own method for this...
class Array
def extract(&block)
temp = self.select(&block)
self.reject!(&block)
temp
end
end
then...
a = [1, 2, 3, 4, 5]
a.extract{|x| x < 3}
=> [1,2]
p a
=> [3, 4, 5]
EDIT: If you don't want to monkey patch (but monkey patching isn't evil in itself) you can do it with a vanilla method...
def select_from_array(array, &block)
temp = array.select(&block)
array.reject!(&block)
temp
end
array = [1,2,3,4,5,6,7,8]
less_than_three = select_from_array(array){|v| v<3}
=> [1,2]
array
=> [3,4,5,6,7,8]
more_than_five = select_from_array(array){|v| v>5}
=> [6,7,8]
array
=> [3,4,5]
In rails 6 there is a method extract!:
a = [1, 2, 3] #=> [1, 2, 3]
a.extract! { |num| num.odd? } #=> [1, 3]
a #=> [2]
irb(main):001:0> array = [1,2,3,4,5,6,7,8]
=> [1, 2, 3, 4, 5, 6, 7, 8]
irb(main):002:0> array.partition{|v| v < 3}
=> [[1, 2], [3, 4, 5, 6, 7, 8]]
is there a specific reason, why this has to be destructive ?
Will this help
class Array
def reject_destructively(&block)
arr = self.select(&block)
arr.each{ |i| self.delete(i) }
arr
end
end
array = [1,2,3,4,5,6,7,8]
p less_than_three = array.reject_destructively{|v| v<3}
#=> [1,2]
p array
#=> [3,4,5,6,7,8]
p more_than_five = array.reject_destructively{|v| v>5}
#=> [6,7,8]
p array
#=> [3,4,5]
The above code can be simplified further to look like:
class Array
def reject_destructively(&block)
self.select(&block).each{ |i| self.delete(i) }
end
end
Ok. This works, avoids monkey patching, keeps it to one line...etc, but it's damn ugly....
less_than_three = array.dup - array.reject!{|v| v<3}
=> [1,2]
array
=> [3,4,5,6,7,8]
more_than_five = array.dup - array.reject!{|v| v>5}
=> [6,7,8]
array
=> [3,4,5]
module Enumerable
def reject_destructively
array=[]
self.each do |y|
if yield(y)
array<<y
end
end
array.each do |x|
self.delete(x)
end
return array
end
end
array=[10,9,2,1,3,45,52]
print less_than_three = array.reject_destructively{|v| v < 3}
print array
You can use group_by to get all of the elements that satisfy the condition in one group, and all of the rest in the other.
For example
[1,2,3,4,5].group_by{|i| i > 3}
gives
{false=>[1, 2, 3], true=>[4, 5]}
More information is available at http://ruby-doc.org/core-2.1.1/Enumerable.html#method-i-group_by

Complete a Ruby Array

My application needs to deal with arrays of fixed size. The problem is that sometimes elements are nil but nil is a forbidden value. I think an easy way is to replace nil values with an the closest non-nil value (right before or right after).
The nil values can be first, last or even multiples. Here are some examples of what I'm looking for:
[1,2,3,nil,5] => [1,2,3,3,5]
[nil,2,3,4,5] => [2,2,3,4,5]
[1,nil,nil,4,5] => [1,1,4,4,5]
I am sure there is an elegant way to do this. Can you help?
My first idea was something like this, now fixed for the general case of arbitrary sequences of nil...
t = nil
p = lambda do |e|
if e.nil?
e,t = t,e
else
t = e
end
e
end
r = a
while r.any? && (r.include? nil)
t = nil; r = r.map(&p)
t = nil; r = r.reverse.map(&p).reverse
end
But I kind of like this one better. (API is arrayObj.merge_all)
module Enumerable
def merge_nil
t = nil
map do |e|
if e.nil?
e,t = t,e
e
else
t = e
end
end
end
end
class Array
def merge_all
return self unless any?
t = self
t = t.merge_nil.reverse.merge_nil.reverse while t.include? nil
t
end
end
You don't really mention what you use the array for, but maybe replacing nil by 0 would make more sense, since it wouldn't influence the result if you want to take averages or something...
[1,2,3,nil,5].map { |el| el ? el : 0 }
It all depends on what you want to do with the data later. It may make sense for you to put in average values but if you have relatively small arrays and are down for a little fun you can go all Bayesian with something like the following:
require 'classifier'
$c = Classifier::Bayes.new
perm = [1, 2, 3, 4, 5].permutation(5)
perm.each { |v| $c.add_category v * "," }
perm.each { |v| $c.train v*"," , v*"," }
def guess(arr)
s = $c.classify(arr*",")
a = s.split(',').map{|s| s.to_i}
end
tests = [
[1,2,3,4,5],
[1,2,3,nil,5],
[nil,2,3,4,5],
[1,nil,nil,4,5],
[1,nil,nil,nil,5],
[nil,nil,3,nil,nil],
[nil,nil,nil,nil,nil]
]
tests.each { |t| puts "Array #{t.inspect} became #{guess(t).inspect}" }
Output looks like the following:
Array [1, 2, 3, 4, 5] became [1, 2, 3, 4, 5]
Array [1, 2, 3, nil, 5] became [1, 2, 3, 4, 5]
Array [nil, 2, 3, 4, 5] became [1, 2, 3, 4, 5]
Array [1, nil, nil, 4, 5] became [1, 2, 3, 4, 5]
Array [1, nil, nil, nil, 5] became [1, 2, 3, 4, 5]
Array [nil, nil, 3, nil, nil] became [1, 2, 3, 4, 5]
Array [nil, nil, nil, nil, nil] became [1, 2, 3, 4, 5]
First, pair up each element with the next and previous elements
triples = array.zip([nil]+array.take(array.length-1), array.drop(1))
Then map over the array of triples like so:
triples.map {|triple|
if triple[0].nil? then
if !triple[1].nil? then triple[1] else triple[2] end
else
triple[0]
end
}
If there are more than 2 nils in a row, this won't work, so put it in a loop and keep calling it until there are no more nils in the array.
EDIT (Jörg W Mittag): You can make this more concise and readable by using destructuring bind and guard clauses:
ary.zip([nil] + ary.take(ary.length-1), ary.drop(1)).map {|prv, cur, nxt|
next prv unless prv.nil?
next cur unless cur.nil?
nxt
}
If you refactor it this way, it becomes easy to see that all the block is doing is looking for the first non-nil element in the previous-current-next triple, which can be more succinctly expressed like this:
ary.zip([nil] + ary.take(ary.length-1), ary.drop(1)).map {|triple|
triple.find {|el| !el.nil? }
}
This, in turn, can be further simplified by using Array#compact.
This is a direct copy of DigitalRoss solution but handling the edge cases of more than two nils in a row. I'm sure DigitalRoss would be able to do this more elegantly, and without the non-idomatic ruby while loop but this works for all the tested cases
def un_nil(arr)
return arr if arr.compact.size == 0 || ! arr.include?(nil)
while arr.include?(nil)
t = nil
p = lambda do |e|
if e.nil?
e,t = t,e
else
t = e
end
e
end
t = nil; r = arr.map(&p)
t = nil; r = r.reverse.map(&p).reverse
arr = r
end
arr
end
tests = [
[1,2,3,4,5],
[1,2,3,nil,5],
[nil,2,3,4,5],
[1,nil,nil,4,5],
[1,nil,nil,nil,5],
[nil,nil,3,nil,nil],
[nil,nil,nil,nil,nil]
]
tests.each {|a| puts "Array #{a.inspect} became #{un_nil(a).inspect}" }
This produces the following output
Array [1, 2, 3, 4, 5] became [1, 2, 3, 4, 5]
Array [1, 2, 3, nil, 5] became [1, 2, 3, 3, 5]
Array [nil, 2, 3, 4, 5] became [2, 2, 3, 4, 5]
Array [1, nil, nil, 4, 5] became [1, 1, 4, 4, 5]
Array [1, nil, nil, nil, 5] became [1, 1, 1, 5, 5]
Array [nil, nil, 3, nil, nil] became [3, 3, 3, 3, 3]
Array [nil, nil, nil, nil, nil] became [nil, nil, nil, nil, nil]
Here's my solution. It will work for any number of nils in the array and gracefully fail if each element in the array is nil. If a nil in the array has a non-nil before and a non-nil after it will randomly pick either before or after.
init and safety check:
arr = [1,nil,nil,4,5]
if arr.nitems == 0
raise "all nil! don't know what to do!"
else
The meat of the solution:
while (arr.index(nil))
arr.each_index do |i|
arr[i] = [arr[i-1], arr[i+1]] [rand 2] if arr[i].nil?
end
end
The wrap-up:
end
arr #print result for review
This has been tested with each of your example instances (nil at start, nil at end, double nil in the middle) and should work for any array size.
Cautions:
The item that comes "before" the first element in the array is the last element
it strikes me that it would be less surprising to propagate the last non-nil value rather than to look ahead for a non-nil value:
def fill_in_array(ary)
last_known = ary.find {|elem| elem} # find first non-nil
ary.inject([]) do |new, elem|
if elem.nil?
new << last_known
else
new << elem
last_known = elem
end
new
end
end
p fill_in_array [1,2,3,nil,5] # => [1,2,3,4,5]
p fill_in_array [1,nil,nil,4,5] # => [1,1,1,4,5]
p fill_in_array [nil,nil,nil,4,5] # => [4,4,4,4,5]
This is a variant of #Callum's solution:
require 'test/unit'
class TestArrayCompletion < Test::Unit::TestCase
def test_that_the_array_gets_completed_correctly
ary = [nil,1,2,nil,nil,3,4,nil,nil,nil,5,6,nil]
expected = [1,1,2,2,3,3,4,4,nil,5,5,6,6]
actual = ary.zip([nil]+ary.take(ary.length-1), ary.drop(1)).
map(&:compact).map(&:first)
assert_equal expected, actual
end
end

Resources