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?
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.
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.inspect
s 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"
My almost the same version
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
@idi527 @kokolegorille thanks! These are great
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"
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?)?
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"
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-####"
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
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', []}