Usernames Suggestion

Hi fellow developers,
I have a list of names i.e [“Jethro”, “Nicholas”, “Michelle”, “Danny1”, “Clifford”] what I’m trying to do is check if in that list there is an exact string say “Jethro” . If it finds a matching string it should suggest another string which has a number appended at the end of the string in this case may be it should append a 3 at the end to make it Jethro3 so that we make sure it does not exist in the list of strings. How can this be done?

What have you tried so far?

I’ve Just managed to check if it exists. But the part of now suggesting a string not in that list is what I have found challenging.

Assuming that you don’t have thousands new users per second and not millions of users with the same base name, you could just recursively check if #{name}_#{i} already exists and if it doesn’t: suggest that. If it does exist just try i+1. MapSet would be better than a list, to quickly find existing users.

2 Likes

I started playing this while eating a sandwich and I went too far.

defmodule Username do
  @existing_usernames [
                        "Jethro",
                        "Jethro1",
                        "Jethro2",
                        "Nicholas",
                        "Michelle",
                        "Danny1",
                        "Clifford"
                      ] ++ Enum.map(1..3000, &"PopularName#{&1}")

  @number_of_suffixes_to_recommend 3
  @batch_size 1000

  @doc """
  splits a username into a tuple
  e.g.
  split("Hello") -> {"Hello", nil}
  split("Hello123") -> {"Hello", 123}
  split("Hello123Goodbye123") -> {"Hello123Goodbye", 123}
  """
  def split(username) when is_binary(username) do
    case Regex.split(~r{[0-9]*$}, username, include_captures: true, trim: true) do
      [prefix, ""] -> {prefix, nil}
      [prefix, suffix] -> {prefix, String.to_integer(suffix)}
    end
  end

  @doc """
  Checks if username already exists.
  If not, returns {:ok, usename}
  If exists, return {:error, alternative_available_usernames_list}
  """
  def check_username(proposed_username) do
    # This is very inefficient! Making a map every time we check the username? That's mad.
    username_map =
      @existing_usernames
      |> Enum.reduce(%{}, fn username, accumulator ->
        {prefix, suffix} = split(username)
        Map.update(accumulator, prefix, MapSet.new([suffix]), &MapSet.put(&1, suffix))
      end)

    {proposed_prefix, proposed_suffix} = split(proposed_username)

    with existing_suffixes when is_map(existing_suffixes) <-
           Map.get(username_map, proposed_prefix),
         true <- MapSet.member?(existing_suffixes, proposed_suffix) do
      alternative_available_usernames_list =
        existing_suffixes
        |> find_available_suffixes()
        |> Enum.map(&"#{proposed_prefix}#{&1}")

      {:error, alternative_available_usernames_list}
    else
      _ -> {:ok, proposed_username}
    end
  end

  defp find_available_suffixes(existing_suffixes, iteration \\ 0) do
    available_suffixes =
      MapSet.new((1 + iteration * @batch_size)..((1 + iteration) * @batch_size))
      |> MapSet.difference(existing_suffixes)

    if MapSet.size(available_suffixes) >= @number_of_suffixes_to_recommend do
      available_suffixes
      |> Enum.take(@number_of_suffixes_to_recommend)
    else
      find_available_suffixes(existing_suffixes, 1 + iteration)
    end
  end
end

for proposed <- ["Jethro3", "Jethro1", "PopularName666"] do
  Username.check_username(proposed)
  |> IO.inspect(label: "result for #{proposed}")
end

You can save that in a file usernames.exs and run elixir usernames.exs to see the output. I get:

result for Jethro3: {:ok, "Jethro3"}
result for Jethro1: {:error, ["Jethro462", "Jethro704", "Jethro417"]}
result for PopularName666: {:error, ["PopularName3590", "PopularName3742", "PopularName3216"]}

I edited the list of existing usernames from OP to include thousands more “PopularName” usernames.

In real life, I would query my list of existing usernames from the database with a WHERE clause to filter on matching the username prefix. Then there would be no need to reduce the list into a map. There would still be the work of splitting the existing usernames but I think that is still faster than having separate suffix/prefix fields in the database (I think?).

2 Likes

:exploding_head: … maybe :slight_smile:

defmodule Suggestion do
  def suggest(names, name) do
    suggest(names, name, name, 0)
  end

  def suggest(names, name, suggested_name, i) do
    if suggested_name in names do
      suggest(names, name, "#{name}_#{i}", i + 1)
    else
      suggested_name
    end
  end
end
1 Like

Name plus number is very hard to remember. Unless you are google or apple that have billions of users, I’d suggest just give people a few more tries and they will find something.