defmodule PostalCode do @moduledoc """ Postal code parser for the Norwegian postal code system. """ alias CSV @posten_file Path.join(File.cwd!(), "postal_codes.csv") |> Path.expand(__DIR__) |> File.read!() |> :unicode.characters_to_binary(:utf8) |> String.split("\n") |> Enum.map(fn x -> x |> String.replace("\r", "") |> String.split("\t") end) @erik_file Path.join(File.cwd!(), "postnummer.csv") |> File.stream!() |> CSV.decode() |> Enum.map(fn x -> case x do {:ok, list} -> Enum.at( Enum.map(list, fn y -> y |> String.replace("\r", "") |> String.split("\t") end), 0 ) end end) def get_erik_file() do @erik_file end @doc """ Accepts a postal code in string format, and returns the postal place. ## Examples iex> PostalCode.postal_code_to_place("0186") {:ok, "OSLO"} iex> PostalCode.postal_code_to_place("0638") :no_results iex> PostalCode.postal_code_to_place(0186) {:error, "Invalid type of postal code, must be string."} iex> PostalCode.postal_code_to_place("018") {:error, "Length must be 4."} """ def postal_code_to_place(postal_code) when is_binary(postal_code) and bit_size(postal_code) == 32 do case get_matching_postal_code(postal_code, :erik) do [_postal_code, place | _rest] -> {:ok, place} nil -> :no_results end end def postal_code_to_place(postal_code) when is_binary(postal_code), do: {:error, :invalid_length} def postal_code_to_place(_postal_code), do: {:error, :invalid_type} def postal_code_to_county(postal_code) when is_binary(postal_code) and bit_size(postal_code) == 32 do case get_matching_postal_code(postal_code, :erik) do [_, _, _, _, _, _, _, _, county, latitude, longitude, _, _, _] -> {:ok, %{ "county" => county, "location" => %{ "latitude" => latitude, "longitude" => longitude } }} nil -> :no_results end end def location_to_county(%{"latitude" => latitude, "longitude" => longitude}) do results = Enum.reduce( @erik_file, %{ "last_diff_long" => 99.99, "last_diff_lat" => 99.99, "longitude" => 0.0, "latitude" => 0.0, "county" => "" }, fn [ _, _, _, _, _, _, _, _, county, postal_lat, postal_lon, _, _, _ ], acc -> with {proper_latitude, _} <- Float.parse(latitude), {proper_longitude, _} <- Float.parse(longitude), {proper_latitude_postal, _} <- Float.parse(postal_lat), {proper_longitude_postal, _} <- Float.parse(postal_lon), diff_latitude <- (proper_latitude - proper_latitude_postal) |> abs, diff_longitude <- (proper_longitude - proper_longitude_postal) |> abs, true <- diff_latitude <= acc["last_diff_lat"] && diff_longitude <= acc["last_diff_long"] do %{ "longitude" => proper_longitude_postal, "latitude" => proper_latitude_postal, "last_diff_long" => diff_longitude |> Float.round(4), "last_diff_lat" => diff_latitude |> Float.round(4), "county" => county } else _ -> acc end end ) if(results["county"] !== "") do {:ok, results["county"]} else {:error, :invalid} end end def postal_code_to_county(postal_code) when is_binary(postal_code), do: {:error, :invalid_length} def postal_code_to_county(_), do: {:error, :invalid_type} def postal_code_to_location(postal_code) when is_binary(postal_code) and bit_size(postal_code) == 32 do case get_matching_postal_code(postal_code, :erik) do [_, _, _, _, _, _, _, _, _, latitude, longitude, _, _, _] -> {latitude, longitude} _ -> :no_results end end def postal_code_to_location(postal_code) when is_binary(postal_code), do: {:error, "Length must be 4."} def postal_code_to_location(_postal_code), do: {:error, "Invalid type of postal code, must be string"} defp get_matching_postal_code(postal_code) do Enum.find(@posten_file, fn x -> Enum.at(x, 0) == postal_code end) end defp get_matching_postal_code(postal_code, :erik) do Enum.find(@erik_file, fn x -> Enum.at(x, 0) == postal_code end) end def get_county_list() do Enum.reduce( @erik_file, MapSet.new([]), fn [ _, _, _, _, _, _, _, _, county, _, _, _, _, _ ], acc -> if(county !== "FYLKE") do MapSet.put(acc, county) else acc end end ) end end