简体   繁体   中英

Calculate the bounding box from a list of points using Elixir

Given a list of points

[
  %{ x: 3, y: 8 },
  ...,
  %{ x: 1, y: 4 }
]

What is the best way to calculate the coordinates of a box that will contain all points - ie the bounds: %{ x1: 1, y1: 4, x2: 3, y2: 8 }

I have a hunch I can use Enum.flat_map_reduce/3 but the syntax is baffling me at the moment.

Enum.reduce/3 would be enough.

input = [%{x: 3, y: 8}, %{x: 1, y: 4}]

Enum.reduce(input, %{x1: nil, x2: nil, y1: nil, y2: nil}, fn
  %{x: x, y: y}, %{x1: x1, x2: x2, y1: y1, y2: y2} ->
    %{
      x1: if(x < x1, do: x, else: x1),
      x2: if(is_nil(x2) or x > x2, do: x, else: x2),
      y1: if(y < y1, do: y, else: y1),
      y2: if(is_nil(y2) or y > y2, do: y, else: y2),
    }
end)

A number in Erlang (and hence Elixir ) is less than any other type, hence nils for x1 and y1 are simply fine. For x2 and y2 we require an additional condition.

I actually ran into this as part of Advent of Code 2018 day 10 last year. This was my method . It sets the bounding box to the initial point, and then expands it as it finds points further out. No need for nil . :-)

Here it is adapted for your data:

# Start with the first point, then compare all the others to find the extremities
def bounding_box([%{x: x, y: y} | points]) do
  Enum.reduce(points, %{x1: x, y1: y, x2: x, y2: y}, fn point, box ->
    %{
      x1: min(point.x, box.x1),
      y1: min(point.y, box.y1),
      x2: max(point.x, box.x2),
      y2: max(point.y, box.y2)
    }
  end)
end

I would propose this, as more readable than an Enum.reduce :

input = [%{x: 3, y: 8}, %{x: 1, y: 4}]

%{
  x1: input |> Enum.map(& &1.x) |> Enum.min(),
  x2: input |> Enum.map(& &1.x) |> Enum.max(),
  y1: input |> Enum.map(& &1.y) |> Enum.min(),
  y2: input |> Enum.map(& &1.y) |> Enum.max()
}

Of course there is the downside of iterating over the list multiple times. Depending on your exact requirements, that might be a problem.

I think Enum.reduce/3 is the right way to go, but Kernel.min/2 and Kernel.max/2 can handle the decision logic pretty well (we just have to augment max to reject nil )

defmodule BoundingBox do
  @moduledoc """
  Creates a bounding box around coordinates.
  """
  @initial %{x1: nil, y1: nil, x2: nil, y2: nil}

  def new(enumerable) do
    Enum.reduce(enumerable, @initial, &get_bounds/2)
  end

  defp get_bounds(%{x: x, y: y}, %{x1: left, y1: bottom, x2: right, y2: top}) do
    %{x1: min(left, x), y1: min(bottom, y), x2: max_num(right, x), y2: max_num(top, right)}
  end

  defp max_num(nil, b), do: b
  defp max_num(a, b), do: max(a, b)
end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM