How to apply mask formatting to a string?

I need to write a function which takes two inputs: a format: "####-####-####", and a string: "123456789876" and returns the string in the format: "1234-5678-9876". Anyone have an idiomatic approach to this?

1 Like

Never used a formatting like that, I usually use sed-style reformatting… ^.^;

If there is not already a library for it though then you should definitely make such a library, looks fairly simple especially if it only works on numbers. :slight_smile:

1 Like

Maybe

Tail recursive version

defmodule Mask do
  @spec mask(String.t(), String.t()) :: String.t()
  def mask(pattern, input) do
    do_mask(pattern, input, "")
  end

  @spec do_mask(String.t(), String.t(), String.t()) :: String.t()
  defp do_mask("#" <> rest_pattern, <<input, rest_input::bytes>>, acc) do
    do_mask(rest_pattern, rest_input, <<acc::bytes, input>>)
  end

  defp do_mask("-" <> rest_pattern, input, acc) do
    do_mask(rest_pattern, input, <<acc::bytes, "-">>)
  end

  defp do_mask(<<>>, _, acc), do: acc
end

Body recursive version

defmodule Mask do
  @spec mask(String.t(), String.t()) :: String.t()
  def mask(pattern, input) do
    do_mask(pattern, input)
  end

  @spec do_mask(String.t(), String.t()) :: String.t()
  defp do_mask("#" <> rest_pattern, <<input, rest_input::bytes>>) do
    <<input, do_mask(rest_pattern, rest_input)::bytes>>
  end

  defp do_mask("-" <> rest_pattern, input) do
    <<"-", do_mask(rest_pattern, input)::bytes>>
  end

  defp do_mask(<<>>, _input), do: <<>>
end

Both produce something like this (if you put IO.inspects in each function clause, this one is for the tail recursive version)

iex(22)> Mask.mask("####-####-####", "123456789876")
#: ["###-####-####", "23456789876", ""]
#: ["##-####-####", "3456789876", "1"]
#: ["#-####-####", "456789876", "12"]
#: ["-####-####", "56789876", "123"]
-: ["####-####", "56789876", "1234"]
#: ["###-####", "6789876", "1234-"]
#: ["##-####", "789876", "1234-5"]
#: ["#-####", "89876", "1234-56"]
#: ["-####", "9876", "1234-567"]
-: ["####", "9876", "1234-5678"]
#: ["###", "876", "1234-5678-"]
#: ["##", "76", "1234-5678-9"]
#: ["#", "6", "1234-5678-98"]
#: ["", "", "1234-5678-987"]
"1234-5678-9876"
5 Likes

My almost the same version :slight_smile:

defmodule TemplateString do
  @joker 35
  
  def render(x, template) when is_binary(template) do
    do_render(Integer.to_string(x), template, [])
  end
  
  defp do_render(_x, <<>>, acc) do
    acc |> Enum.reverse |> to_string
  end
  
  defp do_render(<<>>, _template, acc) do
    acc |> Enum.reverse |> to_string
  end
  
  defp do_render(<<h1, t1::binary>>, <<h2, t2::binary>>, acc) when h2 == @joker do
    do_render(t1, t2, [h1 | acc])
  end
  
  defp do_render(x, <<h, t::binary>>, acc) do
    do_render(x, t, [h | acc])
  end
end

iex> TemplateString.render 123456789876, "####-####-####"
"1234-5678-9876"

BTW It is possible to replace first 2 do_render with

  defp do_render(x, t, acc) when x == "" or t == "" do
    acc |> Enum.reverse |> to_string
  end
3 Likes

@idi527 @kokolegorille thanks! These are great

1 Like

Here’s another shot at this. It goes through each character in the input, replacing the next occurrence of # in the mask with the character. It handles partial fills and over fills just fine.

def fill_mask(input, mask, pos \\ 0)
def fill_mask(input, mask, pos) do
  case String.at(input, pos) do
    nil -> mask
    x -> fill_mask(input, String.replace(mask, "#", x, global: false), pos + 1)
  end
end

iex> fill_mask("123456789876", "####-####-####")
"1234-5678-9876"

iex > fill_mask("12345", "####-####-####")            
"1234-5###-####"

iex> fill_mask("12345678987689", "(###)-###-####, ext ####")
"(123)-456-7898, ext 7689"
2 Likes

If this patterning follows the Windows style hash patterns, then shouldn’t it fill in from the right instead of the left? I.e what if your number is only 42 but the pattern is "####-####-####", then in MS it would output (as I recall) --42, or if you use the pattern "0000-0000-0000" then it would return "0000-0000-0042". Right now all the above things I see seem to rely on the number of numbers being the same as the pattern? And what if it is longer too (I think MS’s things will just put the extra on the left without any other formatting as I recall?)?

1 Like

I would need to reverse the string to achieve that.

defmodule TemplateString do
  @joker [35, 48]
  
  def render(x, template) when is_binary(template) do
    do_render(to_string(x) |> String.reverse, template |> String.reverse, [])
  end
  
  defp do_render(_x, <<>>, acc) do
    acc |> to_string
  end
  
  defp do_render(<<h1, t1::binary>>, <<h2, t2::binary>>, acc) when h2 in @joker do
    do_render(t1, t2, [h1 | acc])
  end
  
  defp do_render(x, <<h, t::binary>>, acc) do
    do_render(x, t, [h | acc])
  end
end

# iex)> TemplateString.render 42, "0000-0000-0000"
# "0000-0000-0042"
1 Like

A slight modification to allow for filling in either direction, and picking the character to replace:

def fill_mask(input, mask, char, direction \\ :right)
def fill_mask(input, mask, char, :right), do: do_fill(input, mask, char, 0)
def fill_mask(input, mask, char, :left) do
  do_fill(String.reverse(input), String.reverse(mask), char, 0)
    |> String.reverse()
end

defp do_fill(input, mask, char, pos) do
  case String.at(input, pos) do
    nil -> mask
    x -> do_fill(input, String.replace(mask, char, x, global: false), char, pos + 1)
  end
end

iex> fill_mask("42", "####-####-####", "#")   
"42##-####-####"

iex> fill_mask("42", "####-####-####", "#", :left) 
"####-####-##42"

iex> fill_mask("42", "####-0000-####", "0", :left)
"####-0042-####"
1 Like

Your solution might not be Tail Call Optimized, because do_fill last statement is a case.

Is it not? I’m not sure about the inner workings of Erlang’s TCO, but there’s no need to get a return value from any of the conditional branches, so shouldn’t this be tail call optimized by the compiler?

If not, this could easily be modified to be more assuredly TCO:

def fill_mask(input, mask, char, direction \\ :right)
def fill_mask(input, mask, char, :right) do
  pos = {0, String.at(input, 0)}
  do_fill(input, mask, char, pos)
end
def fill_mask(input, mask, char, :left) do
  fill_mask(String.reverse(input), String.reverse(mask), char)
    |> String.reverse()
end

defp do_fill(input, mask, char, {_, nil}), do: mask
defp do_fill(input, mask, char, {i, repl_char}) do
  next_pos = {i + 1, String.at(input, i + 1)}
  next_mask = String.replace(mask, char, repl_char, global: false)
  do_fill(input, next_mask, char, next_pos)
end
2 Likes

Another one:

defmodule Unmask do
  def mask_change(mask, unmasked, to_mask) do
    to_mask = List.first(String.to_charlist(to_mask))
    Enum.reduce_while(String.to_charlist(mask), {[], String.to_charlist(unmasked) }, fn
      (_, {acc, []}) -> {:halt, acc}
      (cha, {acc, [h|t]}) when cha == to_mask -> 
        IO.puts("#{inspect acc} - #{inspect h} - #{inspect t}")
        {:cont, {acc ++ [h] , t}}
      (chr, {acc, rem}) -> 
        IO.puts("#{inspect acc} : #{inspect chr} : #{inspect rem}")
        {:cont, {acc ++ [chr], rem}}
    end)
  end
end
iex(84)> test = "####-####-####"                
"####-####-####"
iex(85)> t2 = "123456789012"
"123456789012"
iex(86)> mask_char = "#"
"#"
iex(87)> Unmask.mask_change(test, t2, mask_char)
[] - 49 - '23456789012'
'1' - 50 - '3456789012'
'12' - 51 - '456789012'
'123' - 52 - '56789012'
'1234' : 45 : '56789012'
'1234-' - 53 - '6789012'
'1234-5' - 54 - '789012'
'1234-56' - 55 - '89012'
'1234-567' - 56 - '9012'
'1234-5678' : 45 : '9012'
'1234-5678-' - 57 - '012'
'1234-5678-9' - 48 - '12'
'1234-5678-90' - 49 - '2'
'1234-5678-901' - 50 - []
{'1234-5678-9012', []}
2 Likes