Home > Blockchain >  How to sort only specific elements in an array?
How to sort only specific elements in an array?

Time:06-17

I have an array of mixed elements, e.g. integers and strings:

ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]

and I want to apply a sort but only to specific elements. In the above example, the elements to-be-sorted are the integers (but it could be anything). However, even though they should be sorted, they have to stay in their "integer spots". And the strings have to remain in their exact positions.

The sort should work like this:

[3, "foo", 2, 5, "bar", 1, "baz", 4] # before
[1, "foo", 2, 3, "bar", 4, "baz", 5] # after

"foo" is still at index 1, "bar" is at index 4 and "baz" is at index 6.


I could partition the array into integers and non-integers along with their positions:

a, b = ary.each_with_index.partition { |e, i| e.is_a?(Integer) }

a #=> [[3, 0], [2, 2], [5, 3], [1, 5], [4, 7]]
b #=> [["foo", 1], ["bar", 4], ["baz", 6]]

sort the integers:

result = a.map(&:first).sort
#=>[1, 2, 3, 4, 5]

And re-insert the non-integers at their original positions:

b.each { |e, i| result.insert(i, e) }

result
#=> [1, "foo", 2, 3, "bar", 4, "baz", 5]

But this seems rather clumsy. I particular dislike having to deconstruct and rebuild the array one string at a time. Is there a more elegant or more direct approach?

CodePudding user response:

Possible solution

ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]

integers = ary.select(&->(el) { el.is_a?(Integer) }).sort
ary.map { |n| n.is_a?(Integer) ? integers.shift : n }

# => [1, "foo", 2, 3, "bar", 4, "baz", 5]

CodePudding user response:

I am not proficient with Ruby. Though I'd like to take a shot at what I could come up with.
The idea is as evoked in my comment.

  1. get the indices of the integers
  2. sort the values of the indices
  3. insert the sorted values back into array

ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]
=> [3, "foo", 2, 5, "bar", 1, "baz", 4]
indices = ary.map.with_index { |item,idx| idx if item.is_a?(Integer) }.compact
=> [0, 2, 3, 5, 7]
values = ary.values_at(*indices).sort
=> [1, 2, 3, 4, 5]
indices.zip(values).each { |idx, val| ary[idx]=val}
=> [[0, 1], [2, 2], [3, 3], [5, 4], [7, 5]]
ary
=> [1, "foo", 2, 3, "bar", 4, "baz", 5]

CodePudding user response:

I have assumed that, as in the example, if arr = ary.dup and arr is modified ary is not mutated. If that is not the case one must work with a deep copy of ary.

A helper:

def sort_object?(e)
  e.class == Integer
end
sort_object?(3)
  #=> true
sort_object?(-3e2)
  #=> true
sort_object?("foo")
  #=> false

ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]
obj_to_idx = ary.zip((0..ary.size-1).to_a).to_h
  #=> {3=>0, "foo"=>1, 2=>2, 5=>3, "bar"=>4, 1=>5, "baz"=>6, 4=>7}
to_sort = ary.select { |k| sort_object?(k) }
  #=> [3, 2, 5, 1, 4]
sorted = to_sort.sort
  #=> [1, 2, 3, 4, 5]
new_idx_to_orig_idx = to_sort.map { |n| obj_to_idx[n] }
  .zip(sorted.map { |n| obj_to_idx[n] })
  .to_h
  #=> {0=>5, 2=>2, 3=>0, 5=>7, 7=>3}
new_idx_to_orig_idx.each_with_object(ary.dup) do |(new_idx,orig_idx),a|
  a[new_idx] = ary[orig_idx]
end
  #=> [1, "foo", 2, 3, "bar", 4, "baz", 5]

Some of these statements may of course be chained if desired.

CodePudding user response:

In-Place Re-Assignment at Designated Array Indices

You could certainly make this shorter, and perhaps even skip converting things to and from Hash objects, but the intermediate steps there are to show my thought process and make the intent more explicit and debugging a bit easier. Consider the following:

ary  = [3, "foo", 2, 5, "bar", 1, "baz", 4]

ints = ary.each_with_index.select { |elem, idx| [elem, idx] if elem.kind_of? Integer }.to_h                                         
ordered_ints = ints.keys.sort.zip(ints.values).to_h
ints.keys.sort.zip(ints.values).each { |elem, idx| ary[idx] = elem }

ary
#=> [1, "foo", 2, 3, "bar", 4, "baz", 5]

The idea here is that we:

  1. Select just the items of the type we want to sort, along with their index within the current Array using #each_with_index. If you don't like #kind_of? you could replace it with #respond_to? or any other selection criteria that makes sense to you.
  2. Sort Array values selected, and then #zip them up along with the index locations we're going to modify.
  3. Re-assign the sorted elements for each index that needs to be replaced.

With this approach, all unselected elements remain in their original index locations within the ary Array. We're only modifying the values of specific Array indices with the re-ordered items.

CodePudding user response:

I will throw my hat in this ring as well.

It appears the other answers rely on the assumption of uniqueness so I took a different route by building a positional transliteration Hash (Update: took it a little further than necessary)

def sort_only(ary, sort_klass: Integer)
  raise ArgumentError unless sort_klass.is_a?(Class) && sort_klass.respond_to?(:<=>)
  # Construct a Hash of [Object,Position] => Object
  translator = ary
    .each_with_index
    .with_object({}) {|a,obj| obj[a] = a.first}
    .tap do |t|
      # select the keys where the value (original element) is a sort_klass
      t.filter_map {|k,v| k if v.is_a?(sort_klass)}
        .then do |h| 
          # sort them and then remap these keys to point at the sorted value
          h.sort.each_with_index do |(k,_),idx|
            t[h[idx]] = k
          end 
        end
    end
  # loop through the original array with its position index and use 
  # the transliteration Hash to place them in the correct order
  ary.map.with_index {|a,i| translator[[a,i]]}
end 

Example: (Working Example)

sort_only([3, "foo", 2, 5, "bar", 1, "baz", 4]) 
#=> [1, "foo", 2, 3, "bar", 4, "baz", 5]
sort_only([3, 3, "foo", 2, 5, "bar", 1, "baz",12.0,5,Object, 4,"foo", 7, -1, "qux"])
#=> [-1, 1, "foo", 2, 3, "bar", 3, "baz", 12.0, 4, Object, 5, "foo", 5, 7, "qux"]

sort_only([3, "foo", 2, 5,"qux", "bar", 1, "baz", 4], sort_klass: String)
#=> [3, "bar", 2, 5, "baz", "foo", 1, "qux", 4]

For Reference the Second Example produces the following transliteration:

  {[3, 0]=>-1, [3, 1]=>1, ["foo", 2]=>"foo", [2, 3]=>2, [5, 4]=>3, 
   ["bar", 5]=>"bar", [1, 6]=>3, ["baz", 7]=>"baz", [12.0, 8]=>12.0, 
   [5, 9]=>4, [Object, 10]=>Object, [4, 11]=>5, ["foo", 12]=>"foo", 
   [7, 13]=>5, [-1, 14]=>7, ["qux", 15]=>"qux"}

CodePudding user response:

Below code simply creates two new arrays: numbers and others. numbers are Integer instances from original array, later sorted in descending order. others looks like original array but number spots replaced with nil. at the end push that sorted numbers to nil spots

arr.inject([[], []]) do |acc, el|  
  el.is_a?(Integer) ? (acc[0]<<el; acc[1]<<nil) : acc[1]<<el; acc 
end.tap do |titself| 
  numbers = titself[0].sort_by {|e| -e }
  titself[1].each_with_index do |u, i|
    titself[1][i] = numbers.pop unless u
  end
end.last
  • Related