2

Given an array of modules, what is the best way to return an array that describes the normalized (minimal) ordering relations between the modules? Each element of the array should be an array of pairs of modules that have child-parent relation. The child-parent order within each pair matters, but the order among the pairs does not matter. Normalized ordering means that whatever can be derived from transitivity should be excluded from the array.

For example, given [Object, Comparable, Float, Fixnum, Integer], the answer would be:

[
  [Float, Object],
  [Float, Comparable],
  [Fixnum, Integer],
  [Integer, Object],
  [Integer, Comparable],
]

The five pairs in the array corresponds to the five edges in this Hasse diagram: Hasse diagram

Hint: Module#<=>other returns -1, 0, 1 if there is an order relation, and nil if there is no order relation.

sawa
  • 165,429
  • 45
  • 277
  • 381

2 Answers2

3
def ordering(mods)
  a = mods.permutation(2)
          .select { |m1,m2| (m1<=>m2) == -1 }
  a.reject { |m1,m2|
      mods.any? { |m| a.include?([m1,m]) && a.include?([m,m2]) } }
end

ordering([Object, Comparable, Float, Fixnum, Integer])
  #=> [[Float, Object],
  #    [Float, Comparable],
  #    [Fixnum, Integer],
  #    [Integer, Object],
  #    [Integer, Comparable]] 

mods = [Object, Comparable, Float, Fixnum, Integer, String, Array,
        Hash, Enumerable, Enumerator, Module, Method, IO, File]
ordering(mods)
  #=> [[Float, Object], [Float, Comparable],
  #    [Fixnum, Integer],
  #    [Integer, Object], [Integer, Comparable],
  #    [String, Object], [String, Comparable],
  #    [Array, Object], [Array, Enumerable],
  #    [Hash, Object], [Hash, Enumerable], [Hash, Object],
  #      [Hash, Enumerable],
  #    [Enumerator, Object], [Enumerator, Enumerable],
  #    [Module, Object],
  #    [Method, Object],
  #    [IO, Object], [IO, Enumerable],
  #    [File, IO]]
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • 1
    You could remove the `map` line if `arr` was an array of constants. – Stefan Feb 10 '15 at 09:28
  • I think this answer is not correct. There is a problem here with the `reject` block. Transitivity is not necessarily between adjacent nodes. For example, suppose `A < B < C < D`. The total ordering includes `A < D`. In order to remove this by transitivity, either (`A < B` and `B < D`) or (`A < C` and `C < D`) must be still in the array. However, your algorithm does not guarantee that at least either of `B < D` and `A < C` remains in the array until `A < D` is removed. If `B < D` and `A < C` happen to be removed before `A < D` is removed, then `A < D` will be stuck in the array. – sawa Feb 10 '15 at 09:30
  • Good point, Stefan. I fixed it. sawa, I think it's OK, as `a` is not modified by `reject`. – Cary Swoveland Feb 10 '15 at 10:42
  • Sorry, I was wrong. Now I see the reason you did not chain the methods. – sawa Feb 10 '15 at 14:26
1

It looks like I could introduce the solution. It is far from being elegant, but you might find some parts of this code useful as hints.

I won’t use module comparision.

input = [Object, Comparable, Float, Fixnum, Integer]

First of all, let’s provide a function to build a whole list of class/module supers:

def grands klass
  klasses = klass.included_modules
  loop do
    break unless \
       klass.methods.include?(:superclass) && \
       (klass = klass.superclass)
    klasses << klass
  end 
  klasses
end

Now we would collect all the forward and backward descendants:

result = input.reduce({:fwd => {}, :bwd => {}}) { |memo, klass|
  grands(klass).each { |c| 
    next unless input.include? c
    (memo[:fwd][klass] ||= []) << c 
    (memo[:bwd][c] ||= []) << klass
  }
  memo
}
p result

# NB Below is the output of input _including_ Numeric in demo purposes

# { :fwd => {
#     Float   => [Comparable, Numeric, Object],
#     Fixnum  => [Comparable, Integer, Numeric, Object],
#     Numeric => [Comparable, Object],
#     Integer => [Comparable, Numeric, Object]
#   },
#   :bwd => {
#     Comparable => [Float, Fixnum, Numeric, Integer],
#     Numeric    => [Float, Fixnum, Integer],
#     Object     => [Float, Fixnum, Numeric, Integer],
#     Integer    => [Fixnum]
#   }
# }

It’s time to build the normalized hash:

normalized = Hash[result[:fwd].map { |k, v|
  [k, v.select { |c|
    (result[:bwd][c] & v).empty?
  }]
}]

That gives:

# {  
#    Float   => [Comparable, Object], 
#    Fixnum  => [Integer], 
#    Integer => [Comparable, Object]
# }

You requested an array as the result; the conversion is pretty straightforward and definitely is out of scope of this task.

Hope it helps.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • I think your `:fwd` part can be acheived by `Float.ancestors & input` etc. I am tired to judge your answer now. I will come back again. Thanks. – sawa Feb 10 '15 at 08:48
  • 1
    Yes, I thought about that; `(Float.ancestors & input) - [Float]` should be definitely slower, since we already have everything we need on hand I decided to build everything in-place. – Aleksei Matiushkin Feb 10 '15 at 09:02