This is my response to The Weekly Challenge #287.
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
$str
.
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.