Conditoinal assignment and incrementing

I’m a bit new to Elixir, and I’m trying to conditionally increment a value. This is used as part of determining the complexity of a given password. If a given password contains any digits, I want to increase the keyspace by 10. If the the given password also contains a lowercase letter, I want to increase the keyspace by 26.

Here is a psuedo-code version I have that is mostly Elixir syntax, but typical imperative style:

  def determine_password_complexity(proposed_password) do
    keyspace = 0

    if Regex.match?(~r/[A-Z]/, proposed_password) do
      # The password contains at least one common capital letter.
     
      keyspace = keyspace + 26  # There are 26 common capital letters.
    end
    if Regex.match?(~r/[a-z]/, proposed_password) do
      # The password contains at least one common lowercase letter.

      keyspace = keyspace + 26  # There are 26 common lowercase letters.
    end
    if Regex.match?(~r/[0-9]/, proposed_password) do
      # The password contains at least one digit.

      keyspace = keyspace + 10  # There are 10 common digits.
    end
    if Regex.match?(~r/[^A-Za-z0-9]+/, proposed_password) do
      # The password contains at least one special character.

      keyspace = keyspace + 32  # There are 32 common special characters (inluding one whitespace character).
    end

    password_length = String.length(proposed_password)

    :math.pow(keyspace, password_length)
  end

The first error I run into informs me that keyspace won’t leak out of the conditional, so I re-wrote it as suggested, like this:

  defp determine_password_complexity(proposed_password) do
    keyspace = 0

    keyspace =
      if Regex.match?(~r/[A-Z]/, proposed_password) do
        # The password contains at least one common capital letter.
     
        keyspace + 26  # There are 26 common capital letters.
      end
    keyspace =
      if Regex.match?(~r/[a-z]/, proposed_password) do
        # The password contains at least one common lowercase letter.

        keyspace + 26  # There are 26 common lowercase letters.
      end
    keyspace =
      if Regex.match?(~r/[0-9]/, proposed_password) do
        # The password contains at least one digit.

        keyspace + 10  # There are 10 common digits.
      end
    keyspace =
      if Regex.match?(~r/[^A-Za-z0-9]+/, proposed_password) do
        # The password contains at least one special character.

        keyspace + 32  # There are 32 common special characters (inluding one whitespace character).
      end

    password_length = String.length(proposed_password)

    :math.pow(keyspace, password_length)
  end

When invoked, the code above produces the error bad argument in arithmetic expression which happens on any of the lines that look like keyspace + 26. I assume it’s because I’m trying to use the keyspace variable out of scope? I’m a bit lost on this one…

if false, do: true returns nil, therefore for any falsy condition you set keyspace to nil in your second example. Your first example is invalid due to scoping rules, as you have already been told by the compiler.

You could rewrite your second example to have an explicit else clause which returns the old value of keyspace, eg: ks = if is_digit(c), do: ks + 10, else: ks.

A more idiomatic way were to do it like this:

def complexity(password, keyspace \\ 0, length \\ 0)
def complexity(<<>>, ks, l), do: :math.pow(ks, l)
def complexity(<<d::utf8, rest>>, ks, l) when d in ?0..?9, do: complexity(rest, ks + 10, l + 1)
def complexity(<<a::utf8, rest>>, ks, l) when a in ?a..?z or a in ?A..?Z, do: complexity(rest, ks + 26, l + 1)
def complexity(<<_::utf8, rest>>, ks, l), do: complexity(rest, ks + 32, l + 1)
3 Likes

This is a great example. If you think about what your code has here, it has a set of regexes, and a weight for each regex. We want to build a value by running those rules against your password:

def determine_password_complexity(proposed_password) do
  regexes = %{
    ~r/[A-Z]/ => 26,
    ~r/[a-z]/ => 26,
    ~r/[0-9]/ => 10,
    ~r/[^A-Za-z0-9]+/ => 32
  }

  keyspace = Enum.reduce(regexes, 0, fn {regex, weight}, total ->
    if Regex.match?(regex, proposed_password) do
      total + weight
    else
      total
    end
  end)

  password_length = String.length(proposed_password)

  :math.pow(keyspace, password_length)
end
7 Likes

PS: I just realize, that my implementation is wrong, as it will increase the keyspace for every occurence. I misread the specification. Therefore I think @benwilson512 reply is more appropriate.

1 Like

Thank you for the help, both of you. I’ve tried Ben’s solution and it’s working well. I almost completely understand it, too. :wink: