Replacing characters in a String at a specific Index

Hey! Hope everyone is doing well.

I basically want to replace a character in a string at a specific index. Now, I can’t really convert this to a list using String.graphemes and then recursing over it cause the indexes I am getting is from a list as well and both are of different sizes.

In simpler terms, I have the following list called mylist with the values: [3, 7] and a string called str with characters 123+*12-+3456. Now, the contents inside mylist are the indexes at which I want to replace the characters in the string.

In other words, for the above example, I want to replace the character at index 3, which is +. Similarly, I want to replace the character at index 7. The final string I would get is 123*12+3456.

I tried using a combination of String.at and String.replace, however, the latter replaces all the characters. Is there an efficient way of doing this?

I would write a recursive function that goes through the string once and replaces the character as it goes. Something like (assuming the indices are sorted):

defmodule Replacer do
  def replace(string, indices) do
    replace(string, indices, 0, "")
  end

  def replace(string, [], _current, acc) do
    acc <> string
  end

  def replace(string, [index | indices], current, acc) do
    case String.next_grapheme(string) do
      nil ->
        acc

      {next, rest} ->
        if index == current do
          replace(rest, indices, current + 1, acc <> "_")
        else
          replace(rest, [index | indices], current + 1, acc <> next)
        end
    end
  end
end
5 Likes

This is brilliant! Thank you so much

This is the first thing that comes to my mind

iex(11)> indices = [3, 7]                                                                                                                                                
[3, 7]
iex(12)> replace_by = "_"                                                                                                                                                
"_"
iex(13)> "123+*12-+3456"                                                                                                                                                 
"123+*12-+3456"
iex(14)> |> String.graphemes()                                                                                                                                           
["1", "2", "3", "+", "*", "1", "2", "-", "+", "3", "4", "5", "6"]
iex(15)> |> Enum.with_index()                                                                                                                                            
[
  {"1", 0},
  {"2", 1},
  {"3", 2},
  {"+", 3},
  {"*", 4},
  {"1", 5},
  {"2", 6},
  {"-", 7},
  {"+", 8},
  {"3", 9},
  {"4", 10},
  {"5", 11},
  {"6", 12}
]
iex(16)> |> Enum.map(fn {value, index} -> if index in indices, do: replace_by, else: value end)                                                                          
["1", "2", "3", "_", "*", "1", "2", "_", "+", "3", "4", "5", "6"]
iex(17)> |> to_string()
"123_*12_+3456"
defmodule Replacer do
  def replace(string, indices, replacement \\ ""),
    do: do_replace(string, Enum.sort(indices), 0, replacement)

  defp do_replace(<<>>, _, _, _), do: <<>>
  defp do_replace(<<_::utf8>> <> rest, [idx | indices], idx, replacement),
    do: replacement <> do_replace(rest, indices, idx + 1, replacement)
  defp do_replace(<<c::utf8>> <> rest, indices, idx, replacement),
    do: <<c::utf8>> <> do_replace(rest, indices, idx + 1, replacement)
end
1 Like

This one is just for fun.
Probably works better with lists (so it does not have to iterate over the whole list twice), and big ones,
Uses Enum.while, keeping a copy of the remaining chars, and as soon as your indexes’ list is empty it returns.

index_list = [3, 7]
string = "123+*12-+3456"
replacement = "X"

replacement =
  if replacement == "" do
    nil
  else
    replacement |> String.to_charlist() |> hd()
  end
  
charlist = String.to_charlist(string)

{first, second} = 
  Enum.reduce_while(charlist, {0, index_list, charlist, []}, fn
       # we are done
       _char, {_current_index, [], rest_charlist, acc} ->
          {:halt, {acc, rest_charlist}}

       # replace
       _char, {current_index, [current_index | rest_index_list], rest_charlist, acc} ->
        {:cont, {
          current_index + 1,
          rest_index_list,
          tl(rest_charlist),
          if replacement do
            [replacement | acc]
          else
            acc
          end
        }}

       # move forward
       char, {current_index, index_list, rest_charlist, acc} ->
        {:cont, {current_index + 1, index_list, tl(rest_charlist), [char | acc]}}
     end)

Enum.reverse(first, second) |> List.to_string()