views:

268

answers:

8

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?

+1  A: 

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.

Callum
+4  A: 

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
DigitalRoss
+1 Nice and elegant.
Steve Weet
although it would seem that this doesn't work for all cases...
dustmachine
Right, depending on what the input can look like it may need a loop around the two map lines
DigitalRoss
Ok, fixed for the general case post-clarification...
DigitalRoss
+2  A: 

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 }
Michael Kohl
I'm with you on this one. This is the easy way. Better yet: [1,2,3,nil,5].map { |n| n || 0 }
Ben Marini
You're absolutely right Ben, your version is a lot cleaner :-)
Michael Kohl
A: 

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
dustmachine
A: 

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]
Steve Weet
The part of the guard clause that tests for no nil values at all is not really neeeded as it would just bypass the while loop anyway
Steve Weet
+2  A: 

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]
Drewfer
A: 

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
Jörg W Mittag
A: 

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]
glenn jackman