# It pairs fields of same type with same label name. # It does it in two screenings: # => (1) where the label name is exactly the same. # => (2) with the rest, case insensitive letters (a-z), removed content between brackets # Other features: # => It keeps track on source and destination fields that were not paired # => It allows to remove from the unpaired classification fields that are paired by other means # => It tracks what source fields have multiple candidate destination fields # In other words: it is one of the World Wonders of 2022 !! class Eco::API::UseCases::OozeSamples module HelpersMigration class TypedFieldsPairing include Eco::API::UseCases::OozeSamples::Helpers::Shortcuts EXCLUDED_TYPES = ["tag_field", "chart", "frequency_rate_chart"] attr_reader :src, :dst attr_reader :src_unpaired, :dst_unpaired attr_reader :src_dst_pairs, :src_dst_multi def initialize(source, destination, src_excluded: nil, dst_excluded: nil, exclude_ximport: false) @src = source @dst = destination @src_excluded = src_excluded || [] @dst_excluded = dst_excluded || [] @exclude_ximport = !!exclude_ximport reset! process(exact: true) process end def each_pair src_dst_pairs.each do |(src_field, dst_field)| yield(src_field, dst_field) if block_given? end end def report_src_unpaired(only_present: true, only_indexed: false) return nil if src_unpaired.empty? msg = "Source entry with unpaired fields (#{object_reference(src)}):\n" src_filtered = src_unpaired.values.flatten.reject do |src_fld| only_present && src_fld.empty? end.reject do |src_fld| only_indexed && src_fld.deindex end return nil if src_filtered.empty? msg << src_filtered.map do |src_fld| " • #{object_reference(src_fld)}" end.join("\n") end def report_dst_unpaired(only_required: false, exclude_ximport: true) return nil if dst_unpaired.empty? msg = "Destination entry with unpaired fields (#{object_reference(src)}):\n" dst_filtered = dst_unpaired.values.flatten.reject do |dst_fld| only_required && !dst_fld.required end.reject do |dst_fld| exclude_ximport && ximport?(dst_fld) end return nil if dst_filtered.empty? msg << dst_filtered.map do |dst_fld| " • #{object_reference(dst_fld)}" end.join("\n") end def report_multi_pairs(only_present: true, only_indexed: false, dst_exclude_ximport: true) return nil if src_dst_multi.empty? "".tap do |msg| msg << "Source fields in (#{object_reference(src)}) paired with multiple destination fields:\n" msg_flds = "" src_dst_multi.each do |type, pairs| pairs.each do |(src_fld, dst_flds)| next if (only_present && src_fld.empty?) || (only_indexed && src_fld.deindex) dst_filtered = dst_flds.reject do |dst_fld| dst_exclude_ximport && ximport?(dst_fld) end next if dst_filtered.empty? msg_flds << " • #{object_reference(src_fld)}\n" msg_flds << dst_filtered.map do |dst_fld| " • #{object_reference(dst_fld)}" end.compact.join("\n") end end return nil if msg_flds.empty? msg << msg_flds end end # Remove from unpaired tracking those `flds`. def resolve(*flds) flds.each do |fld| next if unpaired_delete(fld, src_unpaired) unpaired_delete(fld, dst_unpaired) end multi_delete(*flds) end def some_multi? !src_dst_multi.empty? end private # When comparing, use only a-z letters. def only_letters? true end # When comparign, remove anything between brackets from the field label name. def non_bracked? true end # If `false`, it compares with all lower case. def exact? !(only_letters? || non_bracked?) end def process(exact: false) src_unpaired.each do |type, src_pend| next unless (dst_pend = dst_unpaired[type]) && !dst_pend.empty? src_pend.dup.each do |src_fld| dst_candidates = dst_pend.select do |dst_fld| str_eq?(src_fld.label, dst_fld.label, exact: exact) end next if dst_candidates.empty? if dst_candidates.count == 1 pair_add(src_fld, dst_candidates.first) else multi_add(type, src_fld, *dst_candidates) end end end end def str_eq?(str1, str2, exact: false) same_string?(str1, str2, **str_eq_params.merge(exact: exact)) end def str_eq_params { exact: exact?, mild: only_letters?, ignore: false }.tap do |params| params.merge!(ignore: bracked_regex) if non_bracked? end end def pair_add(src_fld, dst_fld) resolve(src_fld, dst_fld) src_dst_pairs << [src_fld, dst_fld] end # Removes a field from a (src or dst) unpaired fields def unpaired_delete(fld, unpaired) type = fld.type return false unless unpaired.key?(type) return false unless (flds = unpaired[type]).delete(fld) unpaired.delete(type) if flds.empty? true end def multi_add(type, src_fld, *dst_flds) (src_dst_multi[type] ||= []) << (pair = [src_fld, dst_flds]) @src_multi[src_fld] = pair dst_flds.each do |dst_fld| (@dst_multi[dst_fld] ||= []) << pair end end def multi_delete(*flds) flds.each do |fld| next unless type_pairs = src_dst_multi[fld.type] if pairs = @dst_multi.delete(fld) pairs.each do |pair| src_fld = pair.first dst_flds = pair.last dst_flds.delete(fld) if dst_flds.empty? @src_multi.delete(src_fld) type_pairs.delete(pair) end end elsif @src_multi.key?(fld) pair = @src_multi.delete(fld) type_pairs.delete(pair) pair.last.each do |dst_fld| pairs = @dst_multi[dst_fld] pairs.delete(pair) @dst_multi.delete(dst_fld) if pairs.empty? end end src_dst_multi.delete(type) if type_pairs.empty? end end def by_type(components) components.to_a.group_by(&:type).tap do |out| EXCLUDED_TYPES.each {|type| out.delete(type)} end end def src_components flds = src.components.to_a flds = flds.reject {|fld| ximport?(fld)} if @exclude_ximport flds - @src_excluded end def dst_components flds = dst.components.to_a flds = flds.reject {|fld| ximport?(fld)} if @exclude_ximport flds - @dst_excluded end def ximport?(fld) fld && fld.tooltip.to_s.include?("ximport") end def reset! @src_unpaired = by_type(src_components) @dst_unpaired = by_type(dst_components) @src_dst_pairs = [] reset_multi! end def reset_multi! @src_dst_multi = {} @src_multi = {} @dst_multi = {} end end end end