defmodule ZambiaMobileNetworks do @moduledoc """ Identify Zambian mobile networks (Airtel, MTN, Zamtel, Zed Mobile) from a phone number. The operator prefix is the two digits after the leading `0` or `260` country code. | Network | Local | With country code | |------------|--------------------|---------------------------| | Airtel | 097, 077, 057 | 26097, 26077, 26057 | | MTN | 096, 076, 056 | 26096, 26076, 26056 | | Zamtel | 095, 075, 055 | 26095, 26075, 26055 | | Zed Mobile | 098, 078\\*, 058\\* | 26098, 26078\\*, 26058\\* | \\* `078`/`058` are reserved for Zed Mobile but not yet officially enabled by the network provider; they are detected pre-emptively. """ @prefixes %{ airtel: ~w(97 77 57), mtn: ~w(96 76 56), zamtel: ~w(95 75 55), # 78/58 reserved but not yet officially enabled by Zed Mobile; detected pre-emptively. zed: ~w(98 78 58) } @type network :: :airtel | :mtn | :zamtel | :zed @doc "Returns the supported networks." @spec networks() :: [network()] def networks, do: Map.keys(@prefixes) @doc "Operator prefixes for a network." @spec prefixes(network()) :: [String.t()] def prefixes(network), do: Map.get(@prefixes, network, []) @doc "Human-readable label for a network." @spec label(network()) :: String.t() def label(:airtel), do: "Airtel" def label(:mtn), do: "MTN" def label(:zamtel), do: "Zamtel" def label(:zed), do: "Zed Mobile" @doc """ Detects the network for an MSISDN, or `nil` if unrecognised. Accepts local (`0XY…`) and country-code (`260XY…`, `+260 XY…`) formats. ## Examples iex> ZambiaMobileNetworks.detect("0971234567") :airtel iex> ZambiaMobileNetworks.detect("260761234567") :mtn iex> ZambiaMobileNetworks.detect("+260 95 123 4567") :zamtel iex> ZambiaMobileNetworks.detect("0981234567") :zed iex> ZambiaMobileNetworks.detect("0911234567") nil """ @spec detect(String.t()) :: network() | nil def detect(msisdn) when is_binary(msisdn) do prefix = msisdn |> String.replace(~r/\D/, "") |> strip_country_code() |> String.slice(0, 2) Enum.find_value(@prefixes, fn {network, prefixes} -> if prefix in prefixes, do: network end) end def detect(_), do: nil @doc """ Whether the MSISDN belongs to the given network. ## Examples iex> ZambiaMobileNetworks.supports?("0961234567", :mtn) true iex> ZambiaMobileNetworks.supports?("0961234567", :airtel) false """ @spec supports?(String.t(), network()) :: boolean() def supports?(msisdn, network), do: detect(msisdn) == network @doc """ Returns the network logo as a base64 `data:` URI (200x200 PNG), or `nil` for an unknown network. Pass `custom` (a file path or raw image binary) to use your own logo instead of the bundled one. The custom image is returned encoded as-is — it is not resized. iex> "data:image/png;base64," <> _ = ZambiaMobileNetworks.logo(:mtn) """ @spec logo(network(), Path.t() | binary() | nil) :: String.t() | nil def logo(network, custom \\ nil) def logo(_network, custom) when is_binary(custom) do bin = if File.regular?(custom), do: File.read!(custom), else: custom "data:image/png;base64," <> Base.encode64(bin) end def logo(network, nil) when network in [:airtel, :mtn, :zamtel, :zed] do path = Application.app_dir(:zambia_mobile_networks, "priv/logos/#{network}.png") "data:image/png;base64," <> Base.encode64(File.read!(path)) end def logo(_network, nil), do: nil defp strip_country_code("260" <> rest), do: rest defp strip_country_code("0" <> rest), do: rest defp strip_country_code(digits), do: digits end