0

I have this array ordered by hash[:points]

original = [{a: "Bill", points: 4}, {b: "Will", points: 3}, {c: "Gill", points: 2}, {d: "Pill", points: 1}]

I want to change the order of it's elements based on the order of a subset array, also ordered by hash[:points].

subset = [{c: "Gill", points: 2}, {b: "Will", points: 1}]

The subset's elements are always contained in the original. But the subset's length and order come at random. It may have two, three, or four elements, in any order.

I want to incorporate the order of the subset into the original array. This can be done by reordering the original, or recreating the original in the correct order. Either will do. But I don't want to merge them. The keys and values in the subset are not important, just the order of the elements.

For example, the above subset should produce this.

# Bill and Pill retain their original position
# Gill and Will swap places as per ordering of the subset
[{a: "Bill", points: 4}, {c: "Gill", points: 2}, {b: "Will", points: 3}, {d: "{Pill}", points: 1}]

Another example with this subset: [{c: "Gill", points: 3}, {b: "Will", points: 2}, {a: "Bill", points: 1}]

# Pill remains at index 3 since it was not in the subset
# Gill, Will, and Bill are reordered based on the order of the subset 
[{c: "Gill", points: 3}, {b: "Will", points: 2}, {a: "Bill", points: 1}, {d: "Pill", points: 1}] 

I've tried a bunch of stuff for the past couple of hours, but I'm finding this harder than it looks.

14
  • {b: 17} is not in the original.. Commented May 29, 2014 at 19:29
  • @ArupRakshit sorry, I'm an idiot. Commented May 29, 2014 at 19:30
  • The keys and values in the subset are not important, just the order of the elements. - Then ordering will be done based on what..? Commented May 29, 2014 at 19:30
  • 2
    What is the supposed order of the elements which are not part of the subset? Commented May 29, 2014 at 19:32
  • 1
    If you and @Arup are both idiots, I am one as well. Let's start a club! Commented May 30, 2014 at 17:15

3 Answers 3

2

My solution has two steps:

  1. Collect the relevant elements from the original array, and sort them according to the subset order.
  2. Replace them in the original array with the new order.

Here is the code:

mapped_elements = subset.map { |i| original.find { |j| j.keys == i.keys } }

result = original.map do |i|
  if subset.find { |j| j.keys == i.keys }
    mapped_elements.shift
  else
    i
  end
end

For subset = [{c: "Gill", points: 2}, {b: "Will", points: 1}] the result will be:

[{a: "Bill", points: 4}, {c: "Gill", points: 2}, {b: "Will", points: 3}, {d: "{Pill}", points: 1}]

For subset = [{c: "Gill", points: 3}, {b: "Will", points: 2}, {a: "Bill", points: 1}] the result will be:

[{c: "Gill", points: 3}, {b: "Will", points: 2}, {a: "Bill", points: 4}, {d: "Pill", points: 1}] 
Sign up to request clarification or add additional context in comments.

9 Comments

Thank you. To clarify my question, I added an additional key to the hashes in the array. It's used for sorting. I feel like a jerk for not including that at the start, but I did not think it mattered. Sorry about that.
Just curious, does providing another hash key make much difference? I omitted that because I felt it was not relevant. Am I right or wrong?
My solution works while all the keys in the item in original appear also in subset, since the code checks for j.keys == i.keys. If only the original had :points, the code would not have worked.
OK. If you are curious about why I need this... it's for sorting world cup tables. The first round of sorting is easy: points, goal diff, and then goals for. If you have a tie, you need to build another table using a subset of group matches that involve only tied teams. This new table is the subset here. When it comes back, it needs to be used to reorder the original without over writing their values. See this: webhome.csc.uvic.ca/~haron/FIFA/#fifa and my related question on here
In other words, the original array is an accurate table. But it needs to be reordered in the event of a tie, based on on a new table that only involves the tied teams. But the original table must remain intact, as it has info based on all the games, not a subset of games between tied teams.
|
1
I know this sounds weird, but trust me, there is a very good reason for it.

Sorry, no can do. I think you chose the wrong structure for your hashes to begin with. I can't think of any reason why you would create hashes which have different keys for each person's name. When you are having trouble manipulating the data structure that you initially chose, then you should think about restructuring your data.

teams = { 
  Bill: {group: "a", points: 4},
  Will: {group: "b", points: 3},
  Gill: {group: "c", points: 2},
  Pill: {group: "d", points: 1},
}

teams_subset = {
  Gill: {group: "c", points: 3}, 
  Will: {group: "b", points: 2}, 
  Bill: {group: "a", points: 1},
}

subset_names =  teams_subset.keys
new_teams = {}

teams.each do |team, stats|
  if teams_subset.include? team
    next_subset_name = subset_names.shift
    new_teams[next_subset_name] = teams[next_subset_name]
  else 
    new_teams[team] = stats
  end
end 

p new_teams

--output:--
{:Gill=>{:group=>"c", :points=>2}, :Will=>{:group=>"b", :points=>3}, :Bill=>{:group=>"a", :points=>4}, :Pill=>{:group=>"d", :points=>1}}

Or even:

teams =  [
  {name: 'Bill', stats: {group: "a", points: 4}},
  {name: 'Will', stats: {group: "b", points: 3}},
  {name: 'Gill', stats: {group: "c", points: 2}},
  {name: 'Pill', stats: {group: "d", points: 1}},
]

Based on your new revelations, I would just use a quasi Schwarzian Transform to convert your data to this form:

"Bill"=>{:name=>"Bill", :points=>4}

...then apply code similar to what I posted above, like this:

require 'set'

teams =  [
  {name: 'Bill', points: 4},
  {name: 'Will', points: 3},
  {name: 'Gill', points: 2},
  {name: 'Pill', points: 1},
]

remapped_teams = {}

teams.each do |hash|
  name = hash[:name]
  remapped_teams[name] = hash
end

p remapped_teams

#--output:--
#{"Bill"=>{:name=>"Bill", :points=>4}, "Will"=>{:name=>"Will", :points=>3}, "Gill"=>{:name=>"Gill", :points=>2}, "Pill"=>{:name=>"Pill", :points=>1}}

teams_subset = [
  {name: 'Gill', points: 3}, 
  {name: 'Will', points: 2}, 
  {name: 'Bill', points: 1},
]

subset_names = teams_subset.map do |hash|
  hash[:name]
end
subset_names_lookup = Set.new(subset_names)

new_team_order = remapped_teams.map do |(name, hash)|
  if subset_names_lookup.include? name
    remapped_teams[subset_names.shift]
  else 
    hash
  end
end 

p new_team_order

--output:--
[{:name=>"Gill", :points=>2}, {:name=>"Will", :points=>3}, {:name=>"Bill", :points=>4}, {:name=>"Pill", :points=>1}]

8 Comments

You can officially call me an idiot. I spent so much time trying to find a solution that when I wrote my examples, I wrote the hashes with different keys. The reality is, all the example hashes have name: "Gill" etc... instad of a:, b:, c:.
The actual structure is [{name: "Foo", points: 1},{ name: "Bar", points: 1}] etc...
@Mohamad, what are the keys for the names?
This is a real example: { name: "USA", goal_diff: 3, points: 6 }, all the hashes have identical keys. To keep things simple I omitted a lot of data, and in the process I confused myself and everyone else.
+1 one for the effort. For the actual application of this, see this comment in this thread.
|
1

Here's another way to do it. This is based on the understanding that each hash in original and in subset contain the same two keys: :name (say) and :points.

Code

def reorder(original, subset)
  orig_hash = original.each_with_object({}) { |h,g| g[h[:name]] = h }
  subset_names = subset.map { |h| h[:name] }
  orig_hash.map { |k,v|
    subset_names.include?(k) ? orig_hash[subset_names.rotate!.last] : v }
end

Examples

original = [{name: "Bill", points: 4}, {name: "Will", points: 3},
            {name: "Gill", points: 2}, {name: "Pill", points: 1}]

#1

subset   = [{name: "Gill", points: 2}, {name: "Will", points: 1}]
reorder(original, subset)
  #=> [{:name=>"Bill", :points=>4}, {:name=>"Gill", :points=>2},
  #    {:name=>"Will", :points=>3}, {:name=>"Pill", :points=>

#2

subset   = [{name: "Gill", points: 3}, {name: "Will", points: 2},
            {name: "Bill", points: 1}]

reorder(original, subset)
  #=> [{:name=>"Gill", :points=>2}, {:name=>"Will", :points=>3},
  #    {:name=>"Bill", :points=>4}, {:name=>"Pill", :points=>1}]

Explanation

The following calculations are performed for original above and

subset = [{c: "Gill", points: 2}, {b: "Will", points: 1}]

Construct this hash from original:

orig_hash = original.each_with_object({}) { |h,g| g[h[:name]] = h }
  #=> {"Bill"=>{:name=>"Bill", :points=>4}, "Will"=>{:name=>"Will", :points=>3},
      "Gill"=>{:name=>"Gill", :points=>2}, "Pill"=>{:name=>"Pill", :points=>1}}

and an array of the values of :name from subsets:

subset_names = subset.map { |h| h[:name] }
  #=> ["Gill", "Will"]

All that remains is to map each k=>v element of orig_hash to either v (if subset_names does not have the key k) or to the first element of subset_names. As we cannot delete the latter from subset_names, I have chosen to rotate the array by +1 and then retrieve that value from the last position. That way, the next key in subset_names will then be positioned properly, at the beginning of the array.

orig_hash.map { |k,v| subset_names.include?(k) ? orig_hash[subset_names.rotate!.last] : v }
  #=> [{:name=>"Bill", :points=>4}, {:name=>"Gill", :points=>2},
  #    {:name=>"Will", :points=>3}, {:name=>"Pill", :points=>1}]

2 Comments

This is pretty cool, thank you. I mentioned in some other comments that that the hash keys are identical. I did not change question because some people had already answered it in it's flawed state. So, my examples should have all had the same keys: [{ a: "Bill"},{ a: "Will" }] instead of [{ a: "Bill" }, { b: "Will" }] notice the keys should be a and a not a and b. Hence my calling myself an idiot :D -- this is pretty neat, though! Thanks for contributing.
I updated my answer to conform with your clarification of the problem. In future, I suggest you clarify by editing you question, rather than trying to do so in comments. It is easy for the reader to miss some comments, and readers will often skip comments made to an answer, on the assumption that those comments pertain only to that answer and not to the question itself. When you edit, it is helpful to indicate with an Edit:, or somesuch.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.