murb/workbook

View on GitHub
lib/workbook/modules/diff_sort.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true
# frozen_string_literal: true

module Workbook
  module Modules
    # Adds essential diffing and comparing support, as well as diffing entire books
    module BookDiffSort
      module ClassMethods
        # Return template table to write the diff result in; in case non exists a default is generated.
        #
        # @return [Workbook::Table] the empty table, linked to a book
        def new_diff_template
          diffbook = Workbook::Book.new
          template = diffbook.template
          f = template.create_or_find_format_by "destroyed"
          f[:background_color] = :red
          f = template.create_or_find_format_by "updated"
          f[:background_color] = :yellow
          f = template.create_or_find_format_by "created"
          f[:background_color] = :lime
          f = template.create_or_find_format_by "header"
          f[:rotation] = 72
          f[:font_weight] = :bold
          f[:height] = 80
          diffbook
        end
      end

      def self.included(base)
        base.extend(ClassMethods)
      end

      # Diff an entire workbook against another, sheet by sheet
      #
      # @param [Workbook::Book] to_workbook to compare against
      # @return [Workbook::Book] workbook with compared result
      def diff to_workbook, options = {sort: true, ignore_headers: false}
        diff_template = Workbook::Book.new_diff_template
        each_with_index do |from_sheet, sheet_index|
          to_sheet = to_workbook[sheet_index]
          if to_sheet
            from_table = from_sheet.table
            to_table = to_sheet.table
            diff_table_template = diff_template.create_or_open_sheet_at(sheet_index).table
            from_table.diff_template = diff_table_template
            from_table.diff(to_table, options)
          end
        end
        diff_template # the template has been filled in the meanwhile, not to use as a template anymore... :)
      end
    end

    # Adds diffing and sorting functions
    module TableDiffSort
      # create an overview of the differences between itself with another 'previous' table, returns a book with a single sheet and table (containing the diffs)
      #
      # @return [Workbook::Table] the return result
      def diff other, options = {}
        options = {sort: true, ignore_headers: false}.merge(options)

        aligned = align(other, options)
        aself = aligned[:self]
        aother = aligned[:other]

        iteration_cols = []
        iteration_cols = if options[:ignore_headers]
          [aother.first.count, aself.first.count].max.times.collect
        else
          (aother.header.to_symbols + aother.header.to_symbols).uniq
        end

        diff_table = diff_template
        maxri = (aself.count - 1)

        (0..maxri).each do |ri|
          row = diff_table.rows[ri] = Workbook::Row.new(nil, diff_table)
          srow = aself.rows[ri]
          orow = aother.rows[ri]

          iteration_cols.each_with_index do |ch, ci|
            scell = srow[ch]
            ocell = orow[ch]
            row[ci] = create_diff_cell(scell, ocell)
          end
        end
        unless options[:ignore_headers]
          diff_table[0].format = diff_template.template.create_or_find_format_by "header"
        end

        diff_table
      end

      # Return template table to write the diff result in; in case non exists a default is generated.
      #
      # @return [Workbook::Table] the empty table, linked to a book
      def diff_template
        return @diff_template if defined?(@diff_template)
        diffbook = Workbook::Book.new_diff_template
        difftable = diffbook.sheet.table
        @diff_template ||= difftable
      end

      # Set the template table to write the diff result in; in case non exists a default is generated. Make sure that
      # the following formats exists: destroyed, updated, created and header.
      #
      # @param [Workbook::Table] table to diff inside
      # @return [Workbook::Table] the passed table
      def diff_template= table
        @diff_template = table
      end

      # aligns itself with another table, used by diff
      #
      # @param [Workbook::Table] other table to align with
      # @param [Hash] options default to: `{:sort=>true,:ignore_headers=>false}`
      def align other, options = {}
        options = {sort: true, ignore_headers: false}.merge(options)

        sother = other.clone.remove_empty_lines!
        sself = clone.remove_empty_lines!

        if options[:ignore_headers]
          sother.header = false
          sself.header = false
        end

        sother = options[:sort] ? sother.sort : sother
        sself = options[:sort] ? sself.sort : sself

        row_index = 0
        while (row_index < [sother.count, sself.count].max) && (row_index < other.count + count)
          row_index = align_row(sself, sother, row_index)
        end

        {self: sself, other: sother}
      end

      def sort
        clone.sort!
      end

      def sort!
        header_row = @rows.delete_at(header_row_index) if header
        @rows = [header_row] + @rows.sort
        self
      end

      private

      # for use in the align 'while' loop
      def align_row sself, sother, row_index
        asd = 0
        if sself.rows[row_index] && sother.rows[row_index]
          asd = sself.rows[row_index].key <=> sother.rows[row_index].key
        elsif sself.rows[row_index]
          asd = -1
        elsif sother.rows[row_index]
          asd = 1
        end
        if (asd == -1) && insert_placeholder?(sother, sself, row_index)
          sother.rows.insert row_index, placeholder_row
          row_index -= 2
        elsif (asd == 1) && insert_placeholder?(sother, sself, row_index)
          sself.rows.insert row_index, placeholder_row
          row_index -= 2
        end

        row_index + 1
      end

      def insert_placeholder? sother, sself, row_index
        (sother.rows[row_index].nil? || !sother.rows[row_index].placeholder?) &&
          (sself.rows[row_index].nil? || !sself.rows[row_index].placeholder?)
      end

      # returns a placeholder row, for internal use only
      def placeholder_row
        return @placeholder_row if defined?(@placeholder_row) && !@placeholder_row.nil?

        @placeholder_row = Workbook::Row.new [nil]
        @placeholder_row.placeholder = true
        @placeholder_row
      end

      # creates a new cell describing the difference between two cells
      #
      # @return [Workbook::Cell] the diff cell
      def create_diff_cell(scell, ocell)
        dcell = scell.nil? ? Workbook::Cell.new(nil) : scell
        if scell == ocell
          dcell.format = scell.format if scell
        elsif scell.nil?
          dcell = Workbook::Cell.new "(was: #{ocell})"
          dcell.format = diff_template.template.create_or_find_format_by "destroyed"
        elsif ocell.nil?
          dcell = scell.clone
          fmt = scell.nil? ? :default : scell.format[:number_format]
          f = diff_template.template.create_or_find_format_by "created", fmt
          f[:number_format] = scell.format[:number_format]
          dcell.format = f
        elsif scell != ocell
          dcell = Workbook::Cell.new "#{scell} (was: #{ocell})"
          f = diff_template.template.create_or_find_format_by "updated"
          dcell.format = f
        end

        dcell
      end
    end
  end
end