# Coordinates carry their aggregation depth; SQL NULL remains an ordinary axis value. class CountsPivotLayout def self.depths(keys, sorts, measures: true) return [] if keys.length < 2 || sorts.first&.first == 'count' || sorts.first&.first&.match?(/\Am[0-9]+\z/) relevant = sorts.map(&:first).select { keys.include?(_1) || (measures && (_1 == 'count' || _1.match?(/\Am[0-9]+\z/))) } order = (relevant + keys).uniq (1...keys.length).select { order.first(_1).sort == keys.first(_1).sort } end def self.label_key(row_count, column_count, row_depth, column_depth) total = row_count + column_count collapsed = total - row_depth - column_depth return nil if collapsed.zero? return 'grandTotal' if total >= 3 && collapsed == total if (collapsed == 1 && ((row_count > 1 && row_depth < row_count) || (column_count > 1 && column_depth < column_count))) return 'subtotal' end 'singleTotal' end def self.headers(values, depths, next_values: nil, grand: false, axis_count: nil, other_count: 0, direction: :row) result = [] values.each_with_index do |tuple, index| result << { values: tuple, kind: 'data' } following = values[index + 1] || next_values depths.reverse_each do |depth| prefix = tuple.first(depth) next if following && following.first(depth) == prefix result << { values: prefix, kind: 'subtotal' } end end result << { values: [], kind: 'grand_total' } if grand count = axis_count || values.first&.length || 0 result.each do |header| depth = header[:values].length label = if header[:kind] != 'data' direction == :row ? label_key(count, other_count, depth, other_count) : label_key(other_count, count, other_count, depth) end header.merge!(depth:, axis_count: count, label_key: label) end result end end