Outputting Tabular Data with IO.ANSI

I would like to display a 2d list for nice tabular output using ANSI escape sequences to control formatting.

So, given this data:

data = [
  [ 100, 20, 30 ],
  [ 20, 10, 20 ],
  [ 50, 400, 20 ]
]

      

I would like to output something like this:

100  20   30
20   10   20
50   400  20

      

Many thanks

+3


source to share


4 answers


Here's a more complex solution that works with arbitrarily wide values:

defmodule Table do
  def format(rows, opts \\ []) do
    padding = Keyword.get(opts, :padding, 1)
    rows = stringify(rows)
    widths = rows |> transpose |> column_widths
    rows |> pad_cells(widths, padding) |> join_rows
  end

  defp pad_cells(rows, widths, padding) do
    Enum.map rows, fn row ->
      for {val, width} <- Enum.zip(row, widths) do
        String.ljust(val, width + padding)
      end
    end
  end

  def join_rows(rows) do
    rows |> Enum.map(&Enum.join/1) |> Enum.join("\n")
  end

  defp stringify(rows) do
    Enum.map rows, fn row ->
      Enum.map(row, &to_string/1)
    end
  end

  defp column_widths(columns) do
    Enum.map columns, fn column ->
      column |> Enum.map(&String.length/1) |> Enum.max
    end
  end

  # http://stackoverflow.com/questions/23705074
  defp transpose([[]|_]), do: []
  defp transpose(rows) do
    [Enum.map(rows, &hd/1) | transpose(Enum.map(rows, &tl/1))]
  end
end

      

Use it like this:



Table.format(data, padding: 2) |> IO.puts

      

Prints:

100  20   30
20   10   20
50   400  20

      

+3


source


You can achieve tab spacing if each element is less than 7 characters long:



iex> Enum.each(data, fn (x) -> Enum.join(x, "\t") |> IO.puts end)
100     20      30
20      10      20
50      400     20
:ok

      

+3


source


The "Elixir Programming" book Dave Thomas is what you need to do todo:

write code to format the data into columns, for example the sample output at the beginning of the chapter:

 #   | Created at           | Title                                             
-----+----------------------+-------------------------------------------------- 
2722 | 2014-08-27T04:33:39Z | Added first draft of File.mv for moving files aro 
2728 | 2014-08-29T14:28:30Z | Should elixir (and other applications) have stick
-----+----------------------+--------------------------------------------------

      

and there is a place where readers can post their solutions for this exercise , where you can find and choose which numbers you have. You will need to modify your code to make it work with your input data structure (an array of arrays, and therefore a matrix), but that should be easy. If you have problems, just ask.

By the way, here is my solution that I wrote while reading the book:

defmodule Issues.TableFormatter do

  import Enum, only: [map: 2, max: 1, zip: 2, join: 2]

  @header ~w(number created_at title)
  @header_column_separator "-+-"

  # TODO: Refactor; try to find more uses for stdlib functions
  def print_table(rows, header \\ @header) do
    table = rows |> to_table(header)
    header = header |> map(&String.Chars.to_string/1) # the headers needs now to be a string
    columns_widths = [header | table] |> columns_widths

    hr = for _ <- 1..(length(header)), do: "-"

    hr     |> print_row(columns_widths, @header_column_separator)
    header |> print_row(columns_widths)
    hr     |> print_row(columns_widths, @header_column_separator)
    table  |> map &(print_row(&1, columns_widths))
    hr     |> print_row(columns_widths, @header_column_separator)
  end

  def to_table(list_of_dicts, header) do
    list_of_dicts
    |> select_keys(header)
    |> map(fn (dict) ->
      dict
      |> Dict.values
      |> map(&String.Chars.to_string/1)
    end)
  end

  def columns_widths(table) do
    table
    |> Matrix.transpose
    |> map(fn (cell) ->
      cell
      |> map(&String.length/1)
      |> max
    end)
  end

  def select_keys(dict, keys) do
    for entry <- dict do
      {dict1, _} = Dict.split(entry, keys)
      dict1
    end
  end

  def print_row(row, column_widths, separator \\ " | ") do
    padding = separator |> String.to_char_list |> List.first # Hack
    IO.puts  row
    |> zip(column_widths)
    |> map(fn ({ cell, column_width }) ->
      String.ljust(cell, column_width, padding)
    end)
    |> join(separator)
  end
end

      

Treat all of this as inspiration, not a direct solution to your problem. It may also be much more than what your needs are, but being able to quickly format some tabular data in general and print it out in your terminal might be very handy for you in the future.

+3


source


There is a printf library for Elixir that makes this kind of simplification easy if you are not familiar with Erlang string generation.

https://github.com/parroty/exprintf

0


source







All Articles