module Eco::API::Common::People::SupervisorHelpers::ClassMethods

Public Instance Methods

identify_cyclic_chains(values) click to toggle source

Identifies all the cyclic supervisor chains @note as `supervisors_tree` will have any entry involved in a cycle at the top, it just checks all the top entries against their offspring @return [Array<Array>] the sets of entries that are cyclic

# File lib/eco/api/common/people/supervisor_helpers.rb, line 39
def identify_cyclic_chains(values)
  raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash)
  return [] unless values && values.is_a?(Enumerable)

  identify  = Proc.new do |top_sup, offspring, chain = [top_sup]|
    next [] if offspring.empty?
    offspring.each_with_object([]) do |(sup, subordinates), set|
      break set unless set.empty?
      if top_sup.supervisor_id == sup.id
        set.concat(chain, [sup])
      else
        set = identify.call(top_sup, subordinates, chain | [sup])
      end
    end
  end

  supervisors_tree(values).each_with_object([]) do |(top_sup, offspring), sets|
    if (set = identify.call(top_sup, offspring)) && !set.empty?
      sets.push(set)
    end
  end
end
print_tree(tree, lev: 0) click to toggle source
sort_by_supervisors(values, supervisors_first: true) click to toggle source

Reorders as follows:

1. supervisors, people with no supervisor or where their supervisor not present
2. subordinates

@return [Array<PersonEntry>] `values` sorted by supervisors/subordinates

# File lib/eco/api/common/people/supervisor_helpers.rb, line 19
def sort_by_supervisors(values, supervisors_first: true)
  raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash)
  return [] unless values && values.is_a?(Enumerable)
  roam = Proc.new do |tree|
    [].tap do |out|
      sub_outs = tree.empty?? [] : tree.map {|sup, subtree| roam.call(subtree)}
      tree.each do |sup, subtree|
        sout = subtree.empty?? [] :roam.call(subtree)
        supervisors_first ? sout.unshift(sup) : sout.push(sup)
        out.concat(sout)
      end
    end
  end

  roam.call(supervisors_tree(values))
end
supervisors_tree(values) click to toggle source

Generates a `Hash` tree structure, where:

* **keys** are nodes
* **values** are `Hash` subtree structures of `key` subordinates

@note it is resilient to cyclic supervisors (it will just add the last at the top) @param values [Enumerable<Object>] of objects with methods:

`id`, `external_id` and `supervisor_id`

@return [Hash] the tree structure

# File lib/eco/api/common/people/supervisor_helpers.rb, line 83
def supervisors_tree(values)
  raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash)
  return {} unless values && values.is_a?(Enumerable)
  idx = get_super_indexes(values)

  processed = []
  subtree = Proc.new do |entry, level, toptree|
    if processed.include?(entry)
      next {} unless toptree.key?(entry) && level > 0
      # needs to be moved as a child
      subnodes = toptree.delete(entry)
      processed.delete(entry)
    end

    subnodes ||= {}.tap do |tree|
      subs = idx[:subordinates].call(entry)
      processed.push(entry)
      next nil unless subs && !subs.empty?
      subs.each do |sub|
        sub_tree = subtree.call(sub, level + 1, toptree)
        tree.merge!(sub_tree)
      end
    end

    {entry => subnodes}
  end

  {}.tap do |tree|
    idx[:by_sup].keys.each do |sup_id|
      if sup = idx[:supers][sup_id]
        tree.merge!(subtree.call(sup, 0, tree))
      else
        idx[:by_sup][sup_id].each do |sub|
          tree.merge!(subtree.call(sub, 0, tree))
        end
      end
    end
  end
end
tree_to_str(tree, lev: 0) click to toggle source
# File lib/eco/api/common/people/supervisor_helpers.rb, line 62
def tree_to_str(tree, lev: 0)
  raise "Required Hash tree structure. Given: #{tree.class}" unless tree.is_a?(Hash)
  "".tap do |str|
    tree.each do |entry, subtree|
      str << "#{"  " * lev}+-- #{entry.id || entry.external_id}\n"
      str << tree_to_str(subtree, lev: lev + 1) unless !subtree || subtree.empty?
    end
  end
end

Private Instance Methods

get_super_indexes(values) click to toggle source
# File lib/eco/api/common/people/supervisor_helpers.rb, line 125
def get_super_indexes(values)
  raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash)
  {}.tap do |indexes|
    indexes[:by_id]  = values.map do |e|
      e.id && [e.id, e]
    end.compact.to_h
    indexes[:by_ext] = values.map do |e|
      e.external_id && [e.external_id, e]
    end.compact.to_h

    indexes[:by_sup] = {}.tap do |by_s|
      values.group_by do |e|
        e.supervisor_id
      end.tap do |by_sup|
        by_s[nil] = by_sup.delete(nil) if by_sup.key?(nil)
        by_s.merge!(by_sup)
      end
    end

    indexes[:supers] = {}.tap do |sups|
      indexes[:by_ext].select do |ext, e|
        ext && indexes[:by_sup].key?(ext)
      end.tap {|found| sups.merge!(found)}
      indexes[:by_id].select do |id, e|
        id && indexes[:by_sup].key?(id)
      end.tap {|found| sups.merge!(found)}
    end

    indexes[:is_super]     = Proc.new do |entry|
      !!(indexes[:supers][entry.id] || indexes[:supers][entry.external_id])
    end

    indexes[:subordinates] = Proc.new do |entry|
      subs = nil
      if sup = indexes[:supers][entry.id] || indexes[:supers][entry.external_id]
        subs ||= indexes[:by_sup][sup.id]          unless !sup.id
        subs ||= indexes[:by_sup][sup.external_id] unless !sup.external_id
      end
      subs
    end
  end

end