defmodule Archeometer.Analysis.DSM.ConsoleRender do
@moduledoc """
Functions to render an `Archeometer.Analysis.DSM` struct into plain text.
"""
alias Archeometer.Analysis.DSM
@v_sep "|"
@h_sep "-"
@ln_sep "\n"
@space " "
@left_margin 1
@doc """
Renders a `DSM` struct into a plain text, suitable for printing in a console.
Returns the string representing the `DSM` in human readable format.
## Parameters
- `dsm`. The `DSM` to be rendered.
- `mod_names`. A map from module ids to their corresponding names.
"""
def render(mtx, mod_names) do
mtx_str(mtx) <>
@ln_sep <>
modules_str(mtx.nodes, mod_names) <>
@ln_sep <>
dependencies(mtx.groups, mod_names)
end
def mtx_str(mtx) do
hd_str = "Design Structure Matrix (DSM):" <> @ln_sep <> @ln_sep
mtx_str = to_string(mtx)
hd_str <> mtx_str <> @ln_sep
end
def mtx_header(%DSM{nodes: nodes}, padding) do
ln_prefix = String.duplicate(@space, padding + @left_margin) <> @v_sep
lines =
nodes
|> Stream.map(fn n ->
n |> to_string() |> String.pad_leading(padding) |> String.graphemes()
end)
# No Enum.zip_with/2 in Elixir 1.10
|> Stream.zip()
|> Stream.map(fn t -> ln_prefix <> (t |> Tuple.to_list() |> to_string()) end)
line_size = length(nodes) + padding + 1 + @left_margin
header_sep = String.duplicate(@h_sep, line_size)
Enum.join(lines, @ln_sep) <> @ln_sep <> header_sep
end
def mtx_rows(%DSM{nodes: nodes, edges: edges}, padding) do
Enum.map_join(nodes, @ln_sep, fn n ->
prefix = (n |> to_string() |> String.pad_leading(padding + @left_margin)) <> @v_sep
# This is sensible to the DSM edges representation
xrefs = Enum.map_join(nodes, fn c -> Map.get(edges, {n, c}, @space) |> to_string() end)
prefix <> xrefs
end)
end
defp modules_str(nodes, mod_names) do
hd_str = "Modules (#{length(nodes)}):" <> @ln_sep <> @ln_sep
node_str = nodes |> modules_detail_str(mod_names)
hd_str <> node_str <> @ln_sep
end
defp modules_detail_str(nodes, mod_names) do
padding = padding(nodes) + 1
Enum.map_join(nodes, @ln_sep, fn m -> module_str(m, mod_names, padding) end)
end
defp module_str(m, mod_names, padding) do
(m |> to_string() |> String.pad_leading(padding)) <> @v_sep <> Map.get(mod_names, m)
end
defp dependencies(groups, mod_names) do
cycles_str(groups, mod_names)
end
def cycles_str(groups, mod_names) do
case map_size(groups) do
0 ->
"No cycles!" <> @ln_sep
_ ->
n_groups = groups |> Map.keys() |> length()
hd_str = "Cycles (#{n_groups}):" <> @ln_sep <> @ln_sep
groups_str = cycles_detail_str(groups, mod_names)
hd_str <> groups_str <> @ln_sep
end
end
defp cycles_detail_str(groups, mod_names) do
groups
|> Map.values()
|> Enum.map(fn modules ->
Enum.map_join(modules, ", ", fn m -> Map.get(mod_names, m) end)
end)
|> Enum.with_index()
|> Enum.map_join(@ln_sep, fn {k, v} -> "Group #{v + 1}: " <> k end)
end
def padding(nodes), do: Enum.max(nodes) |> to_string() |> String.length()
def ln_sep(), do: @ln_sep
defimpl String.Chars, for: DSM do
alias Archeometer.Analysis.DSM.ConsoleRender, as: Render
def to_string(mtx) do
padding = Render.padding(mtx.nodes)
header_str = Render.mtx_header(mtx, padding)
rows_str = Render.mtx_rows(mtx, padding)
header_str <> Render.ln_sep() <> rows_str
end
end
end