Count the number of non-numeric characters in a string and then sum their values

Hello.
Trying to solve this problem I wrote the following algorithm:

string = "3340c9571y40m47ku49t9315mrvzqo667k36e"

# %{
#  "c" => 1,
#  "e" => 1,
#  "k" => 2,
#  "m" => 2,
#  "o" => 1,
#  "q" => 1,
#  "r" => 1,
#  "t" => 1,
#  "u" => 1,
#  "v" => 1,
#  "y" => 1,
#  "z" => 1
#  }
#  [1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1]
# Expected result => 14

def is_numeric?(char) do
    Regex.match?(~r/^\d+$/, char)
end

def total_non_numeric_chars(string) do
    String.codepoints(string)
    |> Enum.reduce(%{}, fn char, acc ->
      cond do
        !is_numeric?(char) ->
          if Map.has_key?(acc, char),
            do: Map.update!(acc, char, &(&1 + 1)),
            else: Map.put(acc, char, 1)

        true ->
          acc
      end
    end)
    |> Map.values()
    |> Enum.sum()
end

I would like to know if is possible to avoid the nested cond and if with some patter matching or other technique.
Any suggestions to improve the algorithm is welcome.

Thanks.

if !is_numeric?(char) do
  Map.update(acc, char, 1, &(&1 + 1))
else
  acc
end
1 Like

Hi @LostKobrakai

Map.update/4 :smiley:

Thanks so much!

Just be aware of the fact that is_numeric? is an unidiomatic name for a function, either we use the prefix is_ when we have guardsave predicates or we use the suffix ? when the predicate is not guardsafe.

3 Likes

Hi @NobbZ
Thank you for your clarification.
So the proper name would be:

def numeric?(char) do
    Regex.match?(~r/^\d+$/, char)
end

Correct?

Since you’re summing them up at the end, I guess you don’t really need to keep the frequency map - you can just keep the sum (unless you also output the map somewhere).

With that in mind, this is how I’d write it:

def total_non_numeric_chars(string), do: loop(string, 0)

defp loop(<<char, rest::binary>>, count) when char in ?0..?9, do: loop(rest, count)
defp loop(<<_, rest::binary>>, count), do: loop(rest, count + 1)
defp loop(<<>>, count), do: count
4 Likes

Caveat:

iex(1)> String.codepoints "noe\u0308l"
["n", "o", "e", "̈", "l"]
iex(2)> String.graphemes "noe\u0308l"
["n", "o", "ë", "l"]
iex(3)> 
1 Like

Hi @michalmuskala
This is a very interesting approach.
Thanks so much for sharing it. :smiley:

I just thought of using the new reduce comprehensions. With that it can be a one-liner:

for << <<char>> <- string >>, char not in ?0..?9, reduce: 0, do: (acc -> acc + 1)
5 Likes

Hello @peerreynders.
Thank you very much for pointing this caveat.

:exploding_head: That is very impressive.

Could you share a link that can take me to the “new reduce comprehensions” documentation?

Thank you very much

1 Like

https://hexdocs.pm/elixir/Kernel.SpecialForms.html#for/1-the-reduce-option

3 Likes

Thank you very much!

It could be a one-liner even without new fancy reduce comprehension option :slight_smile:

length(for << char <- string >>, char not in ?0..?9, do: char)
# or
byte_size(for << char <- string >>, char not in ?0..?9, into: "", do: <<char>>)
2 Likes