Skip to content

Commit

Permalink
Merge pull request #1 from MBXSystems/feature/build-tree
Browse files Browse the repository at this point in the history
Feature/build tree
  • Loading branch information
deepankar-j authored Apr 15, 2024
2 parents 6b77cfc + 40e1ea0 commit db45209
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 44 deletions.
44 changes: 35 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,18 @@ end
For a given list of strings, use `NestedLines.new!/1` to parse the strings into a `%NestedLines{}` struct.

```elixir
lines = ["1", "2", "2.1", "2.2", "3", "3.1", "3.1.1"] |> NestedLines.new!()
["1", "2", "2.1", "2.2", "3", "3.1", "3.1.1"]
|> NestedLines.new!()

# %NestedLines{lines: [[1], [1], [0, 1], [0, 1], [1], [0, 1], [0, 0, 1]]}
```

With a `%NestedLines{}` stuct, you can then output the line numbers using `NestedLines.line_numbers/2`

```elixir
%NestedLines{lines: [[1], [0, 1], [0, 1], [1], [0, 1], [0, 0, 1], [1], [1]]} |> NestedLines.line_numbers()
["1", "1.1", "1.2", "2", "2.1", "2.1.1", "3", "4"]
|> NestedLines.new!()
|> NestedLines.line_numbers()

# ["1", "1.1", "1.2", "2", "2.1", "2.1.1", "3", "4"]
```
Expand All @@ -45,24 +48,47 @@ With a `%NestedLines{}` stuct, you can then output the line numbers using `Neste
Use `NestedLines.indent!/2` and `NestedLines.outdent!/2` to indent and outdent lines, provided they maintain a valid line structure. For example:

```elixir
%NestedLines{lines: [[1], [1], [1]]} |> NestedLines.indent!(2)
["1", "2", "3"]
|> NestedLines.new!()
|> NestedLines.indent!(2)
|> NestedLines.line_numbers()
# Here the line at position 2 CAN be indented, resulting in:
# %NestedLines{lines: [[1], [0, 1], [1]]}
# ["1", "1.1", "2"]

%NestedLines{lines: [[1], [0, 1], [1]]} |> NestedLines.indent!(2)
["1", "1.1", "2"]
|> NestedLines.new!()
|> NestedLines.indent!(2)
# Here the line at position 2 CANNOT be indented further and will raise an ArgumentError
```

Lines that have children can also be indented/outdented and their child lines will also indent/outdent accordingly by one position.

```elixir
%NestedLines{lines: [[1], [0, 1], [0, 0, 1], [1]]} |> NestedLines.outdent!(2)
# NestedLines{lines: [[1], [1], [0, 1], [1]]

%NestedLines{lines: [[1], [1], [0, 1], [1]]} |> NestedLines.outdent!(2)
["1", "1.1", "1.1.1", "2"]
|> NestedLines.new!()
|> NestedLines.outdent!(2)
|> NestedLines.line_numbers()
# ["1", "2", "2.1", "3"]

["1", "2", "2.1", "3"]
|> NestedLines.new!()
|> NestedLines.outdent!(2)
# ArgumentError
```


Use `NestedLines.tree` for a representation of the lines as a nested struct. For example:

```elixir
["1", "1.1", "2"]
|> NestedLines.new!()
|> NestedLines.tree()
# [
# %{line: "1", children: [%{line: "1.1", children: []}]},
# %{line: "2", children: []}
# ]
```

## Contributing

We welcome merge requests for fixing issues or expanding functionality.
Expand Down
128 changes: 118 additions & 10 deletions lib/nested_lines.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ defmodule NestedLines do
## Examples
iex> %NestedLines{lines: [[1], [0, 1], [0, 1], [1], [0, 1], [0, 0, 1]]} |> NestedLines.line_numbers()
iex> NestedLines.new!(["1", "1.1", "1.2", "2", "2.1", "2.1.1"]) |> NestedLines.line_numbers()
["1", "1.1", "1.2", "2", "2.1", "2.1.1"]
"""
Expand All @@ -85,10 +85,10 @@ defmodule NestedLines do
## Examples
iex> %NestedLines{lines: [[1], [0, 1], [0, 1]]} |> NestedLines.can_indent?(1)
iex> NestedLines.new!(["1", "1.1", "1.2"]) |> NestedLines.can_indent?(1)
false
iex> %NestedLines{lines: [[1], [0, 1], [0, 1]]} |> NestedLines.can_indent?(3)
iex> NestedLines.new!(["1", "1.1", "1.2"]) |> NestedLines.can_indent?(3)
true
"""
Expand All @@ -109,8 +109,8 @@ defmodule NestedLines do
## Examples
iex> %NestedLines{lines: [[1], [1], [0, 1], [1]]} |> NestedLines.indent!(4)
%NestedLines{lines: [[1], [1], [0, 1], [0, 1]]}
iex> NestedLines.new!(["1", "2", "2.1", "3"]) |> NestedLines.indent!(4) |> NestedLines.line_numbers()
["1", "2", "2.1", "2.2"]
"""
@spec indent!(t, pos_integer()) :: t
Expand Down Expand Up @@ -142,13 +142,13 @@ defmodule NestedLines do
## Examples
iex> %NestedLines{lines: [[1], [0, 1], [0, 0, 1]]} |> NestedLines.can_outdent?(2)
iex> NestedLines.new!(["1", "1.1", "1.1.1"]) |> NestedLines.can_outdent?(2)
true
iex> %NestedLines{lines: [[1], [0, 1], [1]]} |> NestedLines.can_outdent?(3)
iex> NestedLines.new!(["1", "1.1", "2"]) |> NestedLines.can_outdent?(3)
false
iex> %NestedLines{lines: [[1], [1], [0, 1], [1]]} |> NestedLines.can_outdent?(3)
iex> NestedLines.new!(["1", "2", "2.1", "3"]) |> NestedLines.can_outdent?(3)
true
"""
Expand All @@ -170,8 +170,8 @@ defmodule NestedLines do
## Examples
iex> %NestedLines{lines: [[1], [1], [0, 1], [0, 0, 1]]} |> NestedLines.outdent!(3)
%NestedLines{lines: [[1], [1], [1], [0, 1]]}
iex> NestedLines.new!(["1", "2", "2.1", "2.1.1"]) |> NestedLines.outdent!(3) |> NestedLines.line_numbers()
["1", "2", "3", "3.1"]
"""
def outdent!(%__MODULE__{lines: lines} = nested_lines, position)
Expand Down Expand Up @@ -205,4 +205,112 @@ defmodule NestedLines do

defp outdent_lines(lines), do: Enum.map(lines, fn [0 | line] -> line end)
defp indent_lines(lines), do: Enum.map(lines, &[0 | &1])

@doc """
Returns a boolean indicating if the line at the given position has children
Examples:
iex> NestedLines.new!(["1", "1.1", "1.1.1"]) |> NestedLines.has_children?(1)
true
iex> NestedLines.new!(["1", "1.1", "1.1.1"]) |> NestedLines.has_children?(2)
true
iex> NestedLines.new!(["1", "1.1", "1.1.1"]) |> NestedLines.has_children?(3)
false
iex> NestedLines.new!(["1", "2", "2.1"]) |> NestedLines.has_children?(1)
false
"""
@spec has_children?(t, pos_integer()) :: boolean()
def has_children?(%__MODULE__{lines: lines}, position)
when is_integer(position) and position > 0 do
lines
|> Enum.slice(position - 1, 2)
|> has_children?()
end

defp has_children?([_, [_]]), do: false
defp has_children?([_]), do: false
defp has_children?([_, _]), do: true

@doc """
Returns a tree representation of the input lines.
## Examples
iex> NestedLines.new!(["1", "1.1", "2", "2.1", "2.1.1", "2.2", "2.2.1"]) |> NestedLines.tree()
[
%{
line: "1",
children: [
%{line: "1.1", children: []}
]
},
%{
line: "2",
children: [
%{line: "2.1", children: [
%{line: "2.1.1", children: []}
]},
%{line: "2.2", children: [
%{line: "2.2.1", children: []}
]
}
]
}
]
"""
@spec tree(t) :: list(map())
def tree(%__MODULE__{} = nested_lines) do
nested_lines
|> line_numbers()
|> build_tree([])
end

@spec build_tree(list(String.t()), list(map())) :: list(map())
defp build_tree([], tree), do: Enum.reverse(tree) |> Enum.map(&reverse_children/1)

defp build_tree([line | rest] = lines, tree)
when is_list(lines) and
is_list(tree) and is_binary(line) do
levels = String.split(line, ".")

case levels do
[_] ->
build_tree(rest, [%{line: line, children: []} | tree])

_ ->
updated_tree = add_to_parent(tree, line)
build_tree(rest, updated_tree)
end
end

@spec add_to_parent(list(map()), String.t()) :: list(map())
defp add_to_parent([], line) when is_binary(line), do: [%{line: line, children: []}]

defp add_to_parent([%{line: _} = latest_tree_line | rest], line)
when is_binary(line) do
line_levels = String.split(line, ".")
line_parent_levels = Enum.join(Enum.take(line_levels, Enum.count(line_levels) - 1), ".")

if latest_tree_line.line == line_parent_levels do
updated_line =
Map.update!(latest_tree_line, :children, fn children ->
[%{line: line, children: []} | children]
end)

[updated_line | rest]
else
updated_tree_line = Map.update!(latest_tree_line, :children, &add_to_parent(&1, line))
[updated_tree_line | rest]
end
end

defp reverse_children(%{line: line, children: children}) do
%{line: line, children: Enum.reverse(children) |> Enum.map(&reverse_children/1)}
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule NestedLines.MixProject do
def project do
[
app: :nested_lines,
version: "0.1.1",
version: "0.1.2",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
description: description(),
Expand Down
Loading

0 comments on commit db45209

Please sign in to comment.