How about an excuse to write a bowling score grammar/parser using instaparse:
(def bowl-score-parser
(insta/parser
"S = F F F F F F F F F 10TH
F = OPEN | CLOSED
OPEN = ROLL ROLL
CLOSED = STRIKE | SPARE
10TH = OPEN |
SPARE (STRIKE | ROLL) |
STRIKE (SPARE | ROLL ROLL) |
STRIKE STRIKE (STRIKE | ROLL)
STRIKE = 'X'
SPARE = ROLL '/'
ROLL = PINS | MISS
PINS = #'[1-9]'
MISS = '-'"))
This parses ten frames of a bowling score, with special rules for the tenth frame.
- Each of the first nine frames is either
OPEN (pins left standing) or CLOSED (all pins down).
OPEN is two ROLLs, which are either a number of pins down or a miss. CLOSED is either a STRIKE or SPARE.
10TH describes the possible states for the tenth frame, which is surprisingly confusing! I could've gotten something wrong here.
Here's the parser output for your test case:
(bowling-score-parser "XXXXX6/XX7/XX5")
=>
[:S
[:F [:CLOSED [:STRIKE "X"]]]
[:F [:CLOSED [:STRIKE "X"]]]
[:F [:CLOSED [:STRIKE "X"]]]
[:F [:CLOSED [:STRIKE "X"]]]
[:F [:CLOSED [:STRIKE "X"]]]
[:F [:CLOSED [:SPARE [:ROLL [:PINS "6"]] "/"]]]
[:F [:CLOSED [:STRIKE "X"]]]
[:F [:CLOSED [:STRIKE "X"]]]
[:F [:CLOSED [:SPARE [:ROLL [:PINS "7"]] "/"]]]
[:10TH [:STRIKE "X"] [:STRIKE "X"] [:ROLL [:PINS "5"]]]]
Note: this parser permits an invalid number of total pins in a frame, but you could make a more specific grammar/parser or handle that case post-parsing.