defmodule Masker do @moduledoc """ Tool for finding a 2d pattern in a 2d list. iex> pattern = [[:O, :X, :O], [:O, :X, :O], [:X, :X, :X]] ...> array2d = [ ...> [1, 4, 2, 4, 5], ...> [1, 5, 1, 5, 4], ...> [4, 5, 1, 5, 8], ...> [8, 1, 1, 1, 4], ...> [5, 1, 2, 4, 1] ...> ] ...> Masker.has_pattern?(pattern, array2d) """ @doc """ Find pattern and return coordinates of each match. """ @spec has_pattern?(list, list) :: :none | {:ok, [...]} def has_pattern?([], _), do: :none def has_pattern?(_, []), do: :none def has_pattern?(pattern, array2d) do %{height: p_height, width: p_width} = height_width(pattern) %{height: a_height, width: a_width} = height_width(array2d) if elements_length(pattern) <= elements_length(array2d) && p_height <= a_height && p_width <= a_width do pattern_coordinates = pattern |> keys_coordinates |> Map.values |> Enum.sort pattern_coordinates_size = length(pattern_coordinates) results = 0..(a_height - p_height) |> Enum.map(fn row -> 0..(a_width - p_width) |> Enum.map(fn col -> fragment_coordinates = crop(array2d, p_height, p_width, row, col) |> keys_coordinates |> Map.values if pattern_coordinates_size == length(fragment_coordinates) && Enum.sort(fragment_coordinates) == pattern_coordinates do %{row: row, col: col} end end) end) |> List.flatten |> Enum.reject(&is_nil/1) case results do [] -> :none coordinates -> {:ok, coordinates} end else :none end end @doc """ Crop a 2d list """ @spec crop(list, integer, integer, integer, integer) :: list def crop([], _, _, _, _), do: [] def crop(_, height, width, _, _) when height <= 0 or width <= 0, do: [] def crop(array2d, height, width, row, col) do {row, col} = {negative_to_zero(row), negative_to_zero(col)} %{ height: scoped_height, width: scoped_width } = max_coord_within_bounds(array2d, height, width, row, col) row..(scoped_height - 1) |> Enum.map(fn row_index -> col..(scoped_width - 1) |> Enum.map(fn col_index -> array2d |> Enum.at(row_index) |> Enum.at(col_index) end) end) end defp max_coord_within_bounds(array2d, height, width, row, col) do %{height: ar_height, width: ar_width} = height_width(array2d) scoped_height = row + height scoped_width = col + width %{ height: (if scoped_height <= ar_height, do: scoped_height, else: ar_height), width: (if scoped_width <= ar_width, do: scoped_width, else: ar_width) } end defp negative_to_zero(value), do: if value < 0, do: 0, else: value @spec keys_coordinates(list) :: %{any => list} def keys_coordinates(pattern) do pattern |> Enum.with_index |> Enum.reduce(%{}, fn {row, row_i}, acc_all -> row |> Enum.with_index |> Enum.reduce(acc_all, fn {element, col_i}, acc -> element_instr = case acc[element] do nil -> [[row_i, col_i]] existing -> [[row_i, col_i] | existing] end Map.put(acc, element, element_instr) end) end) end defp height_width(array2d) do %{ height: length(array2d), width: length(Enum.at(array2d, 1)) } end defp elements(matrix) do matrix |> Enum.flat_map(fn li -> Enum.uniq(li) end) |> Enum.uniq end defp elements_length(array2d) do array2d |> elements |> length end end