module Eco::Data::Locations NODE_LEVEL_ATTRS = %i[row_num l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11] NodeLevelStruct = Struct.new(*NODE_LEVEL_ATTRS) # Class to treat input csv in a form of levels, where each column is one level, # and children are placed in higher columns right below the parent. class NodeLevel < NodeLevelStruct include Eco::Data::Locations::NodeBase require_relative 'node_level/cleaner' require_relative 'node_level/parsing' require_relative 'node_level/serial' require_relative 'node_level/builder' extend Eco::Data::Locations::NodeLevel::Builder ALL_ATTRS = NODE_LEVEL_ATTRS ADDITIONAL_ATTRS = %i[row_num] TAGS_ATTRS = ALL_ATTRS - ADDITIONAL_ATTRS attr_accessor :parentId def id tag.upcase end alias_method :nodeId, :id def name tag end def tag clean_id(raw_tag) end def raw_tag values_at(*TAGS_ATTRS.reverse).compact.first end def level actual_level end def actual_level tags_array.compact.length end def raw_level tags_array.index(raw_tag) + 1 end def raw_prev_empty_level tags_array[0..raw_level-1].each_with_index.reverse_each do |value, idx| return idx + 1 unless value end nil end def tag_idx tags_array.index(raw_tag) end def previous_idx idx = tag_idx - 1 idx < 0 ? nil : idx end def empty_idx tary = tags_array tary.index(nil) || tary.length + 1 end # We got a missing level that is compacted in one row # Here we get the missing intermediate levels # This is done from upper to lower level to ensure processing order # It skips last one, as that is this object already def decouple(num = 1) with_info = filled_idxs # must be the last among filled_idxs, so let's use it to verify unless with_info.last == tag_idx raise "Review this (row #{row_num}; '#{raw_tag}'): tag_idx is #{tag_idx}, while last filled idx is #{with_info.last}" end len = with_info.length target_idxs = with_info[len-(num+1)..-2] target_idxs.map do |idx| self.copy.tap do |dup| dup.clear_level(idx_to_level(idx + 1)) end end end def merge!(node) override_upper_levels(node.tags_array) end def set_high_levels(node) override_lower_levels(node.tags_array) end def clear_level(i) case i when Enumerable target = i.to_a when Integer return false unless i >= 1 && i <= tag_attrs_count target = Array(i..tag_attrs_count) else return false end return false if target.empty? target.each do |n| #puts "clearing 'l#{n}': #{attr("l#{n}")}" set_attr("l#{n}", nil) end true end # Cleanses deepest tags def override_upper_levels(src_tags_array, from_level: self.raw_level + 1) target_lev = Array(from_level..tag_attrs_count) target_tags = src_tags_array[level_to_idx(from_level)..level_to_idx(tag_attrs_count)] target_lev.zip(target_tags).each do |(n, tag)| set_attr("l#{n}", tag) end self end # Ensures parent is among the upper level tags def override_lower_levels(src_tags_array, to_level: self.raw_prev_empty_level) return self unless to_level target_lev = Array(1..to_level) target_tags = src_tags_array[level_to_idx(1)..level_to_idx(to_level)] target_lev.zip(target_tags).each do |(n, tag)| set_attr("l#{n}", tag) end self end def idx_to_level(x) x + 1 end def level_to_idx(x) x - 1 end def filled_idxs tags_array.each_with_index.with_object([]) do |(tg, ix), out| out << ix if tg end end def blanks_between? actual_level > empty_idx end def tags_array values_at(*TAGS_ATTRS) end def tag_attrs_count TAGS_ATTRS.length end end end