One could use a single regular expression to validate the string, but I would not recommend it, for the following reasons (when compared to writing high-level language code):
- it would be more difficult to comprehend;
- unit testing would be less straightforward;
- any errors that initially survive unit testing would be harder to spot; and
- it would be more difficult to maintain should there be changes in the specification.
This does not mean that the code should be devoid of regular expressions. I will write example code in Ruby, which should be easily converted to Perl.
It is convenient to first create a helper method that sums the first two digits in a string.
def sum_of_two_digits(s)
s[/\d{2}/].each_char.sum(&:to_i)
end
/\d{2}/
is a regular expression that matches two consecutive digits in a string and s[/\d{2}/]
returns the string comprised of the first two consecutive digits in the string s
.
We can test this with a few strings for which it is needed.
sum_of_two_digits('12') #=> 3
sum_of_two_digits('X12') #=> 3
sum_of_two_digits('12X') #=> 3
sum_of_two_digits('123') #=> 3
We will need a method that determines whether the scoring for each of the first nine frames is valid.
# The string (`s`) for each of the first nine frames must be one of the following:
# 'X'
# 'dd' (two digits whose sum does not exceed 10)
def one_first9_valid?(s)
return true if s == 'X'
s.match?(/\A\d{2}\z/) && sum_of_two_digits(s) <= 10
end
The regular expression reads as follows: "match the beginning of the string (\A
) followed by two digits (\d{2}
, same as \d\d
) followed by the end of the string (\z
)". s.each_char.sum(&:to_i)
sums each character of the string after it has been converted to an integer.
We can try some examples.
one_first9_valid?('12') #=> true
one_first9_valid?('00') #=> true
one_first9_valid?('01') #=> true
one_first9_valid?('1') #=> false
one_first9_valid?('67') #=> false
one_first9_valid?('123') #=> false
one_first9_valid?('X1') #=> false
one_first9_valid?('-1') #=> false
one_first9_valid?('-12') #=> false
We will also need a method that determines whether the scoring for Frame 10 is valid.
# The string (`s`) for Frame 10 must be one of the following:
# 'XXX'
# 'XXd' ('XX' followed by any digit)
# 'Xdd' ('X' followed by 2 digits whose sum does not exceed 10)
# 'dd' (2 digits whose sum does not exceed 10)
# 'ddX' ('X' preceded by 2 digits that sum to 10)
# 'ddd' (2 digits that sum to 10 followed by any digit)
def last_valid?(s)
case s
when /\A(?:XXX|XX\d)\z/
true
when /\AX\d{2}\z/
sum_of_two_digits(s) <= 10
when /\A\d{2}\z/
sum_of_two_digits(s) < 10
when /\A(?:\d{2}X|\d{3})\z/
sum_of_two_digits(s) == 10
else
false
end
end
All four regular expressions require that the beginning (the anchor \A
) and end (the anchor \z
) of the string must be matched. (?:XXX|XX\d)
is a non-capture group that requires the literal 'XXX'
be matched or (|
) the
literal 'XX'
followed by a digit (\d
) be matched. The regular expression /\AX\d{2}\z/
requires that 'X'
followed by 2 digits (\d{2}
) be matched and, because of the anchors, that the string contain no other characters. The other two regular expressions are similar. Notice that when the string s
is comprised of three digits sum_of_two_digits(s)
returns the sum of the first two digits.
Here are some test results.
last_valid?('XXX') #=> true
last_valid?('XX1') #=> true
last_valid?('X12') #=> true
last_valid?('12') #=> true
last_valid?('82X') #=> true
last_valid?('194') #=> true
last_valid?('XXXX') #=> false
last_valid?('XX') #=> false
last_valid?('X1') #=> false
last_valid?('X123') #=> false
last_valid?('1') #=> false
last_valid?('1234') #=> false
last_valid?('Y12') #=> false
last_valid?('X78') #=> false
last_valid?('76') #=> false
last_valid?('76X') #=> false
last_valid?('470') #=> false
We can now put everything together. I will add two puts
statements to identify invalid substrings for the reader.
def valid?(str)
*first9, last = str.split('-')
if (first9.size != 9)
puts "Number of delimiters (#{first9.size}) incorrect"
return false
end
first9.each do |s|
unless (one_first9_valid?(s))
puts "#{s} in first 9 is invalid"
return false
end
end
unless last_valid?(last)
puts "last = #{last} is not valid"
return false
end
true
end
str.split('-')
splits the string on hyphens, returning an array. *first9, last = str.split('-')
assigns the last element of that array to the variable last
and all elements but the last to the array first9
.
We may now test various strings.
valid? "X-91-55-72-X-X-X-90-82-XXX" #=> true
valid? "X-91-55-72-X-X-X-90-82-XX7" #=> true
valid? "X-91-55-72-X-X-X-90-82-X91" #=> true
valid? "X-91-55-72-X-X-X-90-82-90" #=> true
valid? "X-91-55-72-X-X-X-90-82-X91" #=> true
valid? "X-91-55-72-X-X-X-90-82-917" #=> true
valid? "X-91-72-X-X-X-90-82-917" #=> false
Number of delimiters (8) incorrect
valid? "X-91-55-7-X-X-X-90-82-91X" #=> false
7 in first 9 is invalid
valid? "X-91-55-72-X-X-X-X94-82-91X" #=> false
X94 in first 9 is invalid
valid? "X-91-55-72-X-X-X-90-82-X" #=> false
last = X is not valid
valid? "X-91-55-72-X-X-X-90-82-XX" #=> false
last = XX is not valid
valid? "X-91-55-72-X-X-X-90-82-XXXX" #=> false
last = XXXX is not valid
valid? "X-91-55-72-X-X-X-90-82-X92" #=> false
last = X92 is not valid
valid? "X-91-55-72-X-X-X-90-82-91" #=> false
last = 91 is not valid
valid? "X-91-55-72-X-X-X-90-82-92X" #=> false
last = 92X is not valid
valid? "X-91-55-72-X-X-X-90-82-927" #=> false
last = 927 is not valid
-
and checking separate scores in your code. – Cason/
for spare) is quite easy:^(?:(?:X|(\d\/|0\d|1[0-8]|2[0-7]|3[0-6]|4[0-5]|5[0-4]|6[0-3]|7[0-2]|8[01]|90))-){9}(?:XX[X\d]|X(?1)|\d\/[X\d])$
– Cason