Strong and Valid
with Raku

by Arne Sommer

Strong and Valid with Raku

[308] Published 18. September 2024.

This is my response to The Weekly Challenge #287.

Challenge #287.1: Strong Password

Write a program to return the minimum number of steps required to make the given string very strong password. If it is already strong then return 0.

Criteria:
  • It must have at least 6 characters
  • It must contains at least one lowercase letter, at least one upper case letter and at least one digit
  • It shouldn't contain 3 repeating characters in a row
Following can be considered as one step:
  • Insert one character
  • Delete one character
  • Replace one character with another
Example 1:
Input: $str = "a"
Output: 5
Example 2:
Input: $str = "aB2"
Output: 3
Example 3:
Input: $str = "PaaSW0rd"
Output: 0
Example 4:
Input: $str = "Paaasw0rd"
Output: 1
Example 5:
Input: $str = "aaaaa"
Output: 2

I have chosen not to remove characters. Doing so can only solve the 3 repeating characters in a row problem, and inserting a new character in the middle of the repeating characters solves the problem in one step. In addition to potentially adding a missing character type (lowercase, uppercase, digit).

I have also chosen not to swap characters. This is actually not a goofd idea, but I'Ll get back to that leter.

File: strong-password
#! /usr/bin/env raku

unit sub MAIN ($str is copy, :v(:$verbose));                        # [1]

my ($length, $has-len, $has-lc, $has-uc, $has-digit);               # [2]

my $steps = 0;                                                      # [3]

loop                                                                # [4]
{
  $length    = $str.chars;                                          # [5]
  $has-len   = $length >= 6;                                        # [5a]
  $has-lc    = so $str ~~ /<[a..z]>/;                               # [6]
  $has-uc    = so $str ~~ /<[A..Z]>/;                               # [7]
  $has-digit = so $str ~~ /<[0..9]>/;                               # [8]

  my $has-three = so $str ~~ /(.) {} :my $c=$0; >?after $c ** 3>/;  # [9]

  if $verbose
  {
    say ": Length: $length";
    say ": Has OK Length : $has-len";
    say ": Has Lower Case: $has-lc";
    say ": Has Upper Case: $has-uc";
    say ": Has Digit : $has-digit";
    say ": Has 3 repeated chars : $has-three";
  }

  last unless $has-three;                                           # [10]

  my @tokens = tokenizer($str);                                     # [11]

  say ": Tokens: { @tokens.join(";") }" if $verbose;

  my $new = "";                                                     # [12]
  for @tokens -> $token                                             # [13]
  {
    $token.chars >= 3                                               # [14]
      ?? ( $new ~= $token.substr(0,2) ~ get-char-to-add($token)     # [14a]
	         ~ $token.substr(2); $steps++; )                    # [14b]
      !! ( $new ~= $token );
  }
  $str = $new;                                                      # [15]
}

{ $steps++; $str ~= ('a'..'z').pick } unless $has-lc;               # [16]
{ $steps++; $str ~= ('A'..'Z').pick } unless $has-uc;               # [16a]
{ $steps++; $str ~= ('0'..'9').pick } unless $has-digit;            # [16b]

say ": Str: $str" if $verbose;

while $str.chars < 6                                                # [17]
{
  $str ~= get-char-to-add($str);                                    # [17a]
  $steps++;                                                         # [17b]
}

say "Str: $str" if $verbose;

say $steps;                                                         # [18]

sub tokenizer ($str)                                                # [19]
{
  return gather
  {
    my @chars = $str.comb;
    my $first = @chars.shift; 
    my $count = 1; 

    while @chars
    {
      my $second = @chars.shift;
      if $first ne $second
      {
        take $first x $count;
        $first = $second;
        $count = 1;
      }
      else
      {
        $count++;
      }
    }

    take $first x $count;
  }
}

sub get-char-to-add ($string)                                      # [20]
{
  return ("a" .. "z", "0" .. "9").flat.pick if $string ~~ /<[A..Z]>$/;
  return ("a" .. "z", "A" .. "Z").flat.pick if $string ~~ /<[0..9]>$/;
  return ("A" .. "Z", "0" .. "9").flat.pick if $string ~~ /<[a..z]>$/;
}

[1] Note the is copy so that we can change the value (in [15] and later).

[2] These values are available after the loop (in [4]).

[3] The number of steps.

[4] An eternal loop, until we have gotten rid of any 3 repeating characters in a row.

[5] Get the length. Is it long enough [5a]?

[6] Does it have a lowercase letter?

[7] Does it have an uppercase letter?

[8] Does it have a digit?

[9] Does it have any 3 repeating characters?

[10] Exitg the loop if we do not have any 3 repeating characters.

[11] Split the string into tokens, which in this case means an array of characters. Repeating characters are bundled together. (E.g. "abbc" -> "a", "bb", "c".)

[12] The new string (where we have fixed at least one "3 repeating characters") will end up here.

[13] Iterate over the tokens.

[14] Length of three or more? If so, insert a different character after the first two [14a]. If not, keep the string unchanged [14b].

[15] Assign back the modifed string, ready for the next loop iteration.

[16] Add a lowercase letter if the string does not contain one. Ditto for uppercase [16a] and digits [16b]

[17] Is the string long enough? If not, add a character to the end in a loop until it is.

[18] Print the number of steps.

[19] Using gather/take works pretty well here, collecting the characters.

See my Raku Gather, I Take article or docs.raku.org/language/control#gather/take for more information about gather/take.

[20] Get a random character to add. If the last character of the input is an uppercase letter, we add a lowercase letetr or a digit. And so on.

Running it:

$ ./strong-password a
5

$ ./strong-password aB2
3

$ ./strong-password PaaSW0rd
0

$ ./strong-password Paaasw0rd
1

$ ./strong-password aaaaa
2

Looking good.

With verbose mode:

$ ./strong-password -v a
: Length: 1
: Has OK Length : False
: Has Lower Case: True
: Has Upper Case: False
: Has Digit : False
: Has 3 repeated chars : False
: Str: aF7 (after adding missing types)
: Str: aF7V9S (after fixing length)
5

$ ./strong-password -v aB2
: Length: 3
: Has OK Length : False
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : False
: Str: aB2 (after adding missing types)
: Str: aB2Bf3 (after fixing length)
3

$ ./strong-password -v PaaSW0rd
: Length: 8
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : False
: Str: PaaSW0rd (after adding missing types)
: Str: PaaSW0rd (after fixing length)
0

$ ./strong-password -v Paaasw0rd
: Length: 9
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : True
: Tokens: P;aaa;s;w;0;r;d
: Str: PaaRasw0rd

: Length: 10
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : False
: Str: PaaRasw0rd (after adding missing types)
: Str: PaaRasw0rd (after fixing length)
1

$ ./strong-password -v aaaaa
: Length: 5
: Has OK Length : False
: Has Lower Case: True
: Has Upper Case: False
: Has Digit : False
: Has 3 repeated chars : True
: Tokens: aaaaa
: Str: aa2aaa

: Length: 6
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: False
: Has Digit : True
: Has 3 repeated chars : True
: Tokens: aa;2;aaa
: Str: aa2aa4a

: Length: 7
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: False
: Has Digit : True
: Has 3 repeated chars : False
: Str: aa2aa4aS (after adding missing types)
: Str: aa2aa4aS (after fixing length)
3

Oops! The last one gave us a wrong answer this time; 3 instead of 2. By luck. Or bad luck, depending on your point of view...

The culprit is the insertion of the two digits to split up the "3 repeating characters", instead of one digit and one uppercase letter. So we have to add an uppercasse letter afterwards, as an additional third step.

This is easy(ish) to fix:

File: strong-password-2 (partial)
  for @tokens -> $token
  {
    $token.chars >= 3
      ?? ( $new ~= $token.substr(0,2) ~ get-char-to-add($token, $new ~ $str)
             ~ $token.substr(2); $steps++; )                  # [1]
      !! ( $new ~= $token );
  }

[1] Add a second optional argument, the whole string (one and a half time, but that does not matter).

[2] The original version, set up as one of two multiple dispatch canditates with multi.

[3] The new version, with the second argument. This one fixes the problem with the original program, with a lot of code.

See docs.raku.org/syntax/multi for information about multi.

multi sub get-char-to-add ($string)                           # [2]
{
  return ("a" .. "z", "0" .. "9").flat.pick if $string ~~ /<[A..Z]>$/;
  return ("a" .. "z", "A" .. "Z").flat.pick if $string ~~ /<[0..9]>$/;
  return ("A" .. "Z", "0" .. "9").flat.pick if $string ~~ /<[a..z]>$/;
}

multi sub get-char-to-add ($string, $has)                     # [3]
{
  if $string ~~ /<[A..Z]>$/
  {
    return ("a" .. "z").pick unless $has ~~ /<[a..z]>/;
    return ("0" .. "9").pick;
  }

  if $string ~~ /<[a..z]>$/
  {
    return ("A" .. "Z").pick unless $has ~~ /<[A..Z]>/;
    return ("0" .. "9").pick;
  }

  if $string ~~ /<[0..9]>$/
  {
    return ("A" .. "Z").pick unless $has ~~ /<[A..Z]>/;
    return ("a" .. "z").pick;
  }
}

Running this program gives the correct result. Here is one example:

$ ./strong-password-2 -v aaaaa
: Length: 5
: Has OK Length : False
: Has Lower Case: True
: Has Upper Case: False
: Has Digit : False
: Has 3 repeated chars : True
: Tokens: aaaaa
: Str: aaUaaa

: Length: 6
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : False
: Has 3 repeated chars : True
: Tokens: aa;U;aaa
: Str: aaUaa0a

: Length: 7
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : False
: Str: aaUaa0a (after adding missing types)
: Str: aaUaa0a (after fixing length)
2

But we still have one error, not uncovered by the examples:

$ ./strong-password-2 -v A1aaaaa1A
: Length: 9
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : True
: Tokens: A;1;aaaaa;1;A
: Str: A1aa7aaa1A

: Length: 10
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : True
: Tokens: A;1;aa;7;aaa;1;A
: Str: A1aa7aa8a1A

: Length: 11
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : False
: Str: A1aa7aa8a1A (after adding missing types)
: Str: A1aa7aa8a1A (after fixing length)
2

The problem is that I choose to only insert characters. The "a" in the middle of the group of 5 should be swapped with anything else. That one step is the correct answer. My program inserts a "7" after the first two "a"s, leaving three more "a"s for the next iteration of the loop.

The obvious solution is to replace the character, not insert one in addition to it.

File: strong-password-replace (partial)
  for @tokens -> $token
  {
    $token.chars >= 3
      ?? ( $new ~= $token.substr(0,2) ~ get-char-to-add($token, $new ~ $str)
             ~ $token.substr(3); $steps++; )                  # [1]
      !! ( $new ~= $token );
  }

[1] Note the substr(3) instead of substr(2).

It works with the all the examples. Here is the string that did not work out with the previous program:

$ ./strong-password-replace -v A1aaaaa1A
: Length: 9
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : True
: Tokens: A;1;aaaaa;1;A
: Str: A1aa8aa1A

: Length: 9
: Has OK Length : True
: Has Lower Case: True
: Has Upper Case: True
: Has Digit : True
: Has 3 repeated chars : False
: Str: A1aa8aa1A (after adding missing types)
: Str: A1aa8aa1A (after fixing length)
1

Challenge #287.2: Valid Number

You are given a string, $str.

Write a script to find if it is a valid number.

Conditions for a valid number:
  • An integer number followed by an optional exponent
  • A decimal number followed by an optional exponent
  • An integer number is defined with an optional sign '-' or '+' followed by digits
A decimal number is defined with an optional sign '-' or '+' followed by one of the following definitions:
  • Digits followed by a dot '.'
  • Digits followed by a dot '.' followed by digits
  • A dot '.' followed by digits
Exponent:
  • An exponent is defined with an exponent notation 'e' or 'E' followed by an integer number
Example 1:
Input: $str = "1"
Output: true
Example 2:
Input: $str = "a"
Output: false
Example 3:
Input: $str = "."
Output: false
Example 4:
Input: $str = "1.2e4.2"
Output: false
Example 5:
Input: $str = "-1."
Output: true
Example 6:
Input: $str = "+1E-8"
Output: true
Example 7:
Input: $str = ".44"
Output: true

This can almost be done with a single Regex...

File: valid-number
#! /usr/bin/env raku

unit sub MAIN ($str, :v(:$verbose));

say ":Match Object: ", ( $str ~~ /^(<[+-]>?)(\d*)(\.?)(\d*)(<[eE]><[+-]>?\d+)?$/)
 if $verbose;
  ##############################  a # 0 ### # 1 ## 2 ## 3 ## 4 ############## e
 	                                                   # 4a ### 4b ## 4c ###
	  
say so $str ~~ /^(<[+-]>?)(\d*)(\.?)(\d*)(<[eE]><[+-]>?\d+)?$/
  && ($1.Str || $3.Str);

[a] Match from the beginning to the end [e] of the string; i.e. the whole string.

[0] An optional (the trailing ?) + or -, captured in $0.

[1] Zero or more digits, captured in $1.

[2] An optional decimal point, captured in $2.

[3] Zero or more digits, captured in $3.

[4] An optional string (the trailing ?), captured in $4. Consisting of a lower- or uppercase E [4a], an optional sign [4b], and one or more digits [4c].

[5] This will not solve everything, as we need at least one digit from [1] or [3]. This can be done by testing for them, stringified as the match objects in $1 and $3 will be coerced to True in Boolean context even if they did not match.

Note the coercion to a Boolean value done by so.

See docs.raku.org/routine/so for more information about so.

Running it:

$ ./valid-number 1
True

$ ./valid-number a
False

$ ./valid-number .
False

$ ./valid-number 1.2e4.2
False

$ ./valid-number -- -1.
True

$ ./valid-number +1E-8
True

$ ./valid-number .44
True

Looking good.

Note the -- string used to tell Raku to stop parsing the input as command line options in the fifth example, as the very next string will be treated as an option by default - courtesy of the first character -.

With verbose mode:

$ ./valid-number -v 1
:Match Object: 「1」
 0 => 「」
 1 => 「1」
 2 => 「」
 3 => 「」
True

$ ./valid-number -v a
:Match Object: Nil
False

$ ./valid-number -v .
:Match Object: 「.」
 0 => 「」
 1 => 「」
 2 => 「.」
 3 => 「」
False

$ ./valid-number -v 1.2e4.2
:Match Object: Nil
False

$ ./valid-number -v -- -1.
:Match Object: 「-1.」
 0 => 「-」
 1 => 「1」
 2 => 「.」
 3 => 「」
True

$ ./valid-number -v +1E-8
:Match Object: 「+1E-8」
 0 => 「+」
 1 => 「1」
 2 => 「」
 3 => 「」
 4 => 「E-8」
True

$ ./valid-number -v .44
:Match Object: 「.44」
 0 => 「」
 1 => 「」
 2 => 「.」
 3 => 「44」
True

And that's it.