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, ref: "(Row: #{self.row_num}) ") 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 raw_prev_empty_level? lev = raw_prev_empty_level lev && lev > 0 end def raw_latest_consecutive_top_empty_level tags_array[0..raw_level-1].each_with_index do |value, idx| return idx if value end nil end # Requires that all upper levels (lower positions) are filled-in def common_level_with(other) return nil unless other otags_array = other.tags_array.compact stags_array = tags_array.compact raise "Missing lower levels for #{other.id}: #{other.tags_array.pretty_inspect}" unless other.highest_levels_set? raise "Missing lower levels for #{self.id}: #{tags_array.pretty_inspect}" unless highest_levels_set? otags_array.zip(stags_array).each_with_index do |(otag, stag), idx| next if otag&.upcase&.strip == stag&.upcase&.strip return nil if idx == 0 return idx # previous idx, which means prev_idx + 1 (so idx itself) end actual_level end # Second last id in tags_array def raw_parent_id tags_array.compact[-2] end def clean_parent_id clean_tags_array.compact[-2] 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 def copy super.tap do |dup| dup.highest_levels_set! end 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 # @note for each one in the gap, creates a copy and clears deepest levels thereof 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 # This can only happen when there are repeated nodes 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| copy.tap do |dup| dup.clear_level(idx_to_level(idx + 1)) end end end def highest_levels_set? return true if raw_level == 1 return true unless raw_prev_empty_level? !!@highest_levels_set end def highest_levels_set! @highest_levels_set = true end # Sets ancestors def set_high_levels(node, override: false, compact: true) update_lower_levels(node.tags_array, override: override) self end # Clears the deepest levels, from level `i` onwards 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 # Ensures parent is among the upper level tags # It actually ensures all ancestors are there # @param override [Boolean] `false` will only override upmost top consecutive empty levels. def update_lower_levels(src_tags_array, to_level: self.raw_latest_consecutive_top_empty_level, override: false) highest_levels_set! 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)| attr_lev = "l#{n}" set_attr(attr_lev, tag) # unless attr?(attr_lev) && !override 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 clean_tags_array tags_array.map do |tg| clean_id(tg, notify: false) end end def tags_array values_at(*TAGS_ATTRS) end def tag_attrs_count TAGS_ATTRS.length end end end