Pretty ? print of NamedTuple(s)

Proper place for this contribution is perhaps somewhere else but…

Background

Often one is ‘combining’ data from several different objects in many steps. I used to stick to the Relation Algebra pattern with Selection, Projection, Product and Set Operation on Tuples in order to manage complexity.

Quite easy operations to implement in crystal. I do use NamedTuple as dataset. At least for temporary result. But NamedTuple are hard to read (during verification). Or was!! Until now

Here is a test source

# TESTING
# Some NamedTuple (as an array First Normal Form 1NF)
#
data_as_array = [{row: 0, kind: "NARROW", from_name: "TLSapi", connection_line_nr: 0, to_name: "ClientOrServer", to_col_x: 0},
                 {row: 0, kind: "NARROW", from_name: "TLSapi", connection_line_nr: 1, to_name: "FeMaleOrMaleConnection", to_col_x: 1},
                 {row: 0, kind: "NARROW", from_name: "Point", connection_line_nr: 2, to_name: "Inf", to_col_x: 2},
                 {row: 0, kind: "NARROW", from_name: "HKDF", connection_line_nr: 3, to_name: "HMAC", to_col_x: 3},
                 {row: 1, kind: "FAR", from_name: "KeyPair", connection_line_nr: 4, to_name: "Curve", to_col_x: 0}]
# Print it
puts pretty_named_tuples(data_as_array, "data_as_array")

#
#  Make a Hash(key,value)
#
data_as_hash = data_as_array.group_by { |r| r[:row] }
# Print it (here just the first 3 lines only)
puts pretty_named_tuples(data_as_hash.values.flatten[0, 3], "data_as_hash")

# Or with less data and for one certain key
# 
less_data = data_as_array
  .map { |row| {from_name: row[:from_name], to_name: row[:to_name]} }
  .group_by { |r| r[:from_name] }
# 
puts pretty_named_tuples(less_data["TLSapi"], "less_data")

and the result

data_as_array
=============
| row: 0 | kind: NARROW | from_name:  TLSapi | connection_line_nr: 0 | to_name:         ClientOrServer | to_col_x: 0 |
| row: 0 | kind: NARROW | from_name:  TLSapi | connection_line_nr: 1 | to_name: FeMaleOrMaleConnection | to_col_x: 1 |
| row: 0 | kind: NARROW | from_name:   Point | connection_line_nr: 2 | to_name:                    Inf | to_col_x: 2 |
| row: 0 | kind: NARROW | from_name:    HKDF | connection_line_nr: 3 | to_name:                   HMAC | to_col_x: 3 |
| row: 1 | kind:    FAR | from_name: KeyPair | connection_line_nr: 4 | to_name:                  Curve | to_col_x: 0 |
data_as_hash
============
| row: 0 | kind: NARROW | from_name: TLSapi | connection_line_nr: 0 | to_name:         ClientOrServer | to_col_x: 0 |
| row: 0 | kind: NARROW | from_name: TLSapi | connection_line_nr: 1 | to_name: FeMaleOrMaleConnection | to_col_x: 1 |
| row: 0 | kind: NARROW | from_name:  Point | connection_line_nr: 2 | to_name:                    Inf | to_col_x: 2 |
less_data
=========
| from_name: TLSapi | to_name:         ClientOrServer |
| from_name: TLSapi | to_name: FeMaleOrMaleConnection |

The source itself (like 40 rows only)

# Usage here is 'crystal run pretty_named_tuples.cr'
# ------------------------------------------------------------
def self.pretty_named_tuples(the_named_tuple_rows, title)
  ret = IO::Memory.new
  if the_named_tuple_rows.size == 0
    return ret << "#{title} (empty)"
  end
  total_widths_cols = Array(Int32).new
  col_names = Array(String).new
  # Scan all rows! to determine col widths
  the_named_tuple_rows
    .each_with_index { |arow, col_nr|
      widths_in_row = arow
        .map { |acol|
          if col_nr == 0
            col_names << acol.to_s # Pick col names from first row
          end
          acol.to_s.size + ((arow[acol].to_s).size) + 1
        }
      if total_widths_cols.size == 0
        total_widths_cols = widths_in_row # First column in first row!
      end
      total_widths_cols.each_with_index { |a_col_width, col_index|
        total_widths_cols[col_index] = [total_widths_cols[col_index], widths_in_row[col_index]].max
      }
    }
  return ret << title << "\n" << title.size.times.map { |i| "=" }.join << "\n" <<
    the_named_tuple_rows.map { |arow|
      col_names.map_with_index { |col_name, col_nr|
        col_content = arow[col_names[col_nr]]
        if col_nr == 0
          left_margin = "|"
        else
          left_margin = ""
        end
        col_name_s = col_name + ":"
        col_content_s = col_content.to_s
        spc_cnt : Int32 = total_widths_cols[col_nr] - col_name_s.size - col_content_s.size
        spacing = ((1..spc_cnt).map { |c| " " }).join
        left_margin + " " + col_name_s + spacing + " " + col_content_s + " |"
      }.join # join columns into a row
    }.join("\n")
end

Combine test source and method source into the_thing.cr

And use the very handsome crystal run the_thing.cr to understand how to use your own data perhaps

Oh, really nice!

Small question: even if it is small, do you plan to publish it in Github?