# Parse a Baseball Pitch String

Take an arbitrarily sized string as input. This string represents a baseball plate appearance, in which each character represents a pitch as follows:

• Strike: S
• Ball: B
• Foul ball: F
• Hit by pitch: H
• Ball in play: X

(For those wondering, this is a very very simplified version of Retrosheet's notation)

Your program must output 1 of 3 possible outputs to signify 1 of the mutually exclusive outcomes:

• Strikeout
• Walk
• Ball in play

It doesn't matter what the outputs are exactly, as long as they are guaranteed to be distinct.

For those unfamiliar with the rules of baseball:

• 3 strikes results in a strikeout
• 4 balls results in a walk
• A foul ball is a strike UNLESS the batter already has 2 strikes, in that case nothing happens
• Hit by pitch immediately results in a walk
• "Ball in play" immediately results in the "Ball in play" outcome

You may assume that:

• the input string is encoded as ASCII
• the input string represents an entire plate appearance (in other words it will end in one of the 3 outcomes above)
• there are no other characters other than the ones above

You may not assume that:

• there are no extra pitches/characters after the plate appearance is supposed to legally end
• your program must return on the last pitch/character

Examples:

"Strikeout"fy:

SSS
SBSBS
BBBSSS
BSBSBS
BSFFBFS
BBSSFFFFFFFFS
SSSBBBB
FSS


"Walk"y:

BBBFB
BBBB
BBBSB
SSBBBB
SSH
SBSFFBFFFFBFFFFFFFFB
BBBBSSS
HSSS
SBBBBSS


"Ball in play"ly:

X
SSX
BBBX
SBSX
FFFFFFFFFFFFFFX
XBSBSBSB


This is , so fewest bytes wins.

(This challenge was inspired by this YouTube video)

• How does XBSBSBSB work? The ball's in play, and then there's a strikeout, and then a fourth ball and it ends up in play? Apr 17, 2020 at 20:48
• @Noodle9 "You may not assume that there are no extra pitches/characters after the plate appearance is supposed to legally end." This is a dirty input case. Your program should return after parsing the X, effectively ignoring the rest of the characters. Apr 17, 2020 at 20:52
• oh, duh, I meant this one is closely related. Apr 17, 2020 at 21:14
• It seems everything about the ball in play is missing. I don't know baseball terminology so I have no idea what sort of state this even indicates. Could you explain this for people who don't know baseball terminology? Apr 19, 2020 at 1:43
• What does this mean: "You must not asumme that: ... your program must return on the last pitch/character" ? I can't find any sensible interpretation for this sentence? Apr 20, 2020 at 13:13

# Retina 0.8.2, 22 bytes

4=B
H
2F|S

1![XHS]


Try it online! Link includes test cases. Outputs X for ball in play, H for walk, and S for strikeout. Explanation:

4=B
H


The fourth ball results in a walk, same as hit by pitch.

2F|S


The first two foul balls/strikes are ignored.

1![XHS]


Take the first available result.

# JavaScript (ES9),  68 66  55 bytes

Saved 11 bytes thanks to @Neil!

Returns $$\S\$$ for Strikeout, $$\X\$$ for Ball in play or an empty string for Walk.

s=>/(?<=(B.*){4})|(?=H)|X|(?<=([SF].*){2})S/.exec(s)[0]


Try it online!

### How?

We match either:

• (?<=(B.*){4}) : an empty string preceded by 4 B's
• (?=H) : an empty string followed by an H
• X : the character X
• (?<=([SF].*){2})S : an S preceded by 2 other strike characters (S or F)

As a result, we get an S for a Strikeout, an X for a Ball in play, or an empty string for a Walk.

• (S|F) can be [SF], but if S, X and (empty string) are acceptable outputs, you can just use s=>/(?<=(B.*){4})|(?=H)|X|(?<=([SF].*){2})S/.exec(s)[0].
– Neil
Apr 18, 2020 at 10:12
• I have a "SyntaxError: invalid regexp group" (because non-fixed width lookbehind it seems) when I try in the console or in Regex101 (or with PHP, no I swear I wasn't about to port the answer).. How can it work for NodeJS? Apr 20, 2020 at 10:30
• @Kaddath This should work fine in recent versions of Chrome, Edge or Node, but not yet in Firefox. Apr 20, 2020 at 10:57
• And so should I conclude that ES9 supports non-fixed width lookbehinds (although not on all browsers) while PCRE doesn't? Apr 20, 2020 at 14:52
• @Kaddath I think so, but I didn't check the ECAMScript spec. Apr 20, 2020 at 15:24

# Python 3, 908886 85 bytes

-2 bytes thanks to @ovs

s=b=0
for i in map(ord,input()):s+=~i%2*~s*i%71<3;b+=i<67;b//4+s//3==i%4//2>exit(i%6)


Try it online!

Returns via exit code: 0 for a walk, 4 for ball-in-play, and 5 for a strikeout.

• The last statement can be b//4+s//3>=(i%8<1)>exit(i%6) for -2.
– ovs
Apr 19, 2020 at 18:11
• @ovs Thanks! I had to adjust it slightly to get it to work, but I used the same idea Apr 19, 2020 at 19:14

# 05AB1E, 28262422 20 bytes

Ç.Δ6Ö½$¾4@N¾Ì@)yè}6%  Try it online! Outputs 5 for strike out, 0 for walk, or 4 for ball in play. Ç # convert the input to a list of ASCII codepoints .Δ } # find the first codepoint y such that: 6Ö # is y divisible by 6? (true for B and H only) ½ # if yes, increment the counter variable c$             #  push 1 and input
¾4@          #  is c >= 4?
N¾Ì@      #  is the iteration count N >= c + 2?
)     #  wrap the stack in a list: [1, input, c >= 4, N >= v + 2]
yè   #  get the y-th element of that list (wraps around)

# * H and X are mapped to 1, so those characters always result in a match
# * nothing is mapped to input, it's just there to pad the list
# * B and F are mapped to c >= 4, so the 4th B results in a match (this would also
#   match HBBB or BBBBF, but we stop after the first match, so that's irrelevant)
# * S is mapped to N >= c + 2, so an S matches if it's preceded by at least 2 characters not in (B, H)

6%                  # after the loop: modulo 6 (B => 0, H => 0, S => 5, X => 4)

• Very nice! Got a lot of small things that makes the total great. :) Like the $ to push a 1 for both X and H, as well as a filler value (the input) that won't be indexed. Or the 6Ö½ to only increase the counter variable for H and B. Apr 22, 2020 at 10:30 # SNOBOL4 (CSNOBOL4), 172 bytes  I =INPUT N I LEN(1) . P REM . I :($P)
S	S =LT(S,2) S + 1	:F(K)S(N)
B	B =LT(B,3) B + 1	:F(H)S(N)
F	S =LT(S,2) S + 1	:(N)
X	OUTPUT =0	:(E)
K	OUTPUT =1	:(E)
H	OUTPUT =2
E
END


Try it online!

0 for a ball in play, 1 for a strikeout, and 2 for a walk.

gosh, I miss baseball :-(

Thanks to Mitchell Spector for pointing out several bugs!

• I like seeing a SNOBOL answer, but this seems to be incorrect for SSBBBBS -- it says it's a strike-out, but it should be a walk. tio.run/##VY0xC8IwEIXny6@4wSGHRUx1KmQJXiG0xuCltLOz2MH/… Apr 18, 2020 at 18:45
• @MitchellSpector I think the LE(B,3) should be LT(B,3). I've always had trouble with off-by-one issues. Apr 18, 2020 at 23:09
• I see what you're doing now -- it looks good. The only issue remaining is not handling H (batter hit by pitch), but that's easily taken care of! Apr 19, 2020 at 19:13
• @MitchellSpector thanks, sorry, it's been a busy few weeks. May 18, 2020 at 21:04

# Retina 0.8.2, 42 41 bytes

Thanks to @Neil for saving a byte!

(?<=(B.*){3})B
H
1!X|H|(?<=([SF].*){2})S


Try it online!

Based on @Arnauld's JavaScript answer.

Returns X for ball-in-play, H for a walk, and S for a strikeout.

• (S|F) can be [SF].
– Neil
Apr 18, 2020 at 10:13

# C (gcc), 124 $$\\cdots\$$ 87 86 bytes

Saved 12 13 bytes thanks to ceilingcat!!!
Saved 6 bytes thanks to dingledooper!!!

c;b;s;f(char*p){for(b=s=0;c=*p++-66,s+=c==4&s<2|c==17,b+=!c,c!=6&b<4&s<3&c<22;);c%=3;}


Try it online!

Returns $$\0\$$ for a walk, $$\1\$$ for a ball in play, and $$\2\$$ for a strikeout.

# Io, 142 bytes

Returns 0 for a strikeout, 5 for a walk, 4 for a ball-in-play.

method(I, s :=b :=0
I foreach(i,s=s+i%2+if((s-1)*i%69<3,1,0);b=b+if(i<67,1,0);if(((b/4)floor!=0)or(i%8<1)or((s/3)floor!=0),System exit(i%6))))


Try it online!

• 121 bytes: Try it online! (Note that strikeout returns 5, walk returns 0 for this one) Apr 19, 2020 at 16:07

# 05AB1E, 32 bytes

ηε.•Çšy•uS¢+3@y'SÅ¿*·s4@r;M}0Kн


Outputs 2 for Strikeout; 1 for Walk; and 0.5 for Ball in play.

Could be 31 bytes by removing the u if we can take the input as lowercase.

Explanation:

η              # Get all prefixed of the (implicit) input-string
ε             # Map each prefix to:
.•Çšy•       #  Push compressed string "xhbsf"
u      #  Convert it to uppercase: "XHBSF"
S     #  Convert it to a list of characters: ["X","H","B","S","F"]
¢    #  Count each character in the current prefix-string
            #  Push the counts separated to the stack
+           #  Add the counts of "S" and "F" together
3@         #  Check that it's >= 3
*   #  And:
y'SÅ¿   '#  Check whether the current prefix ends with an "S"
·  #  And double this combined check (so 2 if truthy; 0 if falsey)
s           #  Swap so the count of 'B' is at the top of the stack
4@         #  Check that it's >= 4 (1 if truthy; 0 if falsey)
r           #  Reverse the stack, so the count of 'X' is at the top of the stack
;          #  Halve it (0.5 if truthy; 0.0 if falsey)
M            #  Push the largest value of the stack
}0K           # After the map: remove all 0s
н          # And pop and push the first value of the list
# (after which it is output implicitly as result)


Note that this assumes the count of "H" and "X" can never be larger than 1, which we can due to the assumptions mentioned in the challenge description.

See this 05AB1E tip of mine (section How to compress strings not part of the dictionary?) to understand why .•Çšy• is "xhbsf".

# [C#], 147 bytes

d=(p,s,b,i)=>{var P=p[i];int t=0;if(P=='B')b++;if(P=='S'||(P=='F'&&s<2))s++;if(P=='H'||b>3)t=2;if(P=='X')t=3;if(s>2)t=1;return t!=0?t:d(p,s,b,++i);};


Try It online!

Return values: 1 = Strikeout, 2 = Walk, 3 = Ball in play.

(I wanted to use recursion for this answer, for learning)

• Hi, welcome to CGCC! If you're using a recursive method, the d= should be counted towards the byte-count as well, so this is actually 149 bytes. But, you can golf this to 122 bytes by nesting ternary-ifs; changing the characters to code-point integers for the checks; and changing all || and && to | and & respectively (except for the || in P=='S'||(P=='F'&&s<2), since we need it to get rid of the parenthesis). Apr 20, 2020 at 9:43
• Also, if you haven't seen it yet, tips for golfing in C# and tips for golfing in <all languages> might both be interesting to read through. Enjoy your stay! :) Apr 20, 2020 at 9:43
• Thanks Kevin, I just saw this, I'll check it out, and improve my answer! Apr 28, 2020 at 0:02
• 119 bytes Oct 31, 2020 at 8:03

# Z80Golf, 49 bytes

00000000: 0603 0e04 cd03 80fe 5320 0105 fe42 2001  ........S ...B .
00000010: 0dfe 4620 0310 0104 fe48 2001 4afe 5828  ..F .....H .J.X(
00000020: 0caf b93e 5728 06af b820 d93e 53cd 0080  ...>W(... .>S...
00000030: 76                                       v


Try it online!

Prints W for walk, S for strikeout, and X for ball in play.

## Ungolfed

        ld b,3  ; number of strikes until strikeout
ld c,4  ; number of balls until walk
input:
call $8003 ; A = next character from stdin strike: ; if A == 'S', record a strike cp 'S' jr nz,ball dec b ball: ; if A == 'B', record a ball cp 'B' jr nz, foul dec c foul: ; if A == 'F', cp 'F' jr nz, hit_by_pitch ; record a strike ; jump to the next section if batter hasn't struck out djnz hit_by_pitch ; if batter has struck out, give them another chance ; foul balls can't strike out a batter inc b hit_by_pitch: cp 'H' jr nz, ball_in_play ld c, d ; d is initialized to 0 by default ball_in_play: cp 'X' jr z, exit walk: xor a ; 1 byte shorter than ld a, 0 cp c ld a, 'W' jr z, exit strikeout: xor a cp b jr nz, input ld a, 'S' exit: call$8000
halt


# sed -E, 69 60 59 bytes

s/([FS][^FS]*){2}S/&K/;s/([^B]*B){4}/&H/;s/[^KHX]*(.).*/\1/


Try it online!

9 bytes off, thanks to math junkie.

Input on stdin.

Output on stdout: H for walk, K for strikeout, X for in play.

• I think you can save some bytes by returning H and X instead of w and p. I don't know sed too well though, so I could be wrong Apr 19, 2020 at 15:25
• @mathjunkie Good idea -- thanks! Apr 19, 2020 at 15:45

# Charcoal, 52 bytes

≔⪪Ｓ¹θＦ⪪B34HF02SS02F⁴Ｆ✂⌕Ａθ§ι⁰Ｉ§ι¹Ｉ§ι²§≔θκ§ι³§Φθ№XHSι⁰


Try it online! Link is to verbose version of code. Outputs X, H or S as appropriate. Explanation:

≔⪪Ｓ¹θ


Split the input string into an array of characters. (If that is a legal input format, then these 5 bytes could be removed, but it would be really hard to enter in any of the examples.)

Ｆ⪪B34HF02SS02F⁴


Iterate over a string literal which encodes the following rules: The fourth Ball counts as a Hit by pitch. The first two Fouls might count as Strikes. The first two Fouls or Strikes don't strike the batsman out so turn them back into Fouls.

Ｆ✂⌕Ａθ§ι⁰Ｉ§ι¹Ｉ§ι²


Find the range of matches of the source character that we're interested in.

§≔θκ


Update those matches with the destination character.

§ι³§Φθ№XHSι⁰


Filter out all characters except X, H and S and output the first remaining character.

## Javascript, 84 characters

z=>{y={B:-1,S:i=0,F:0};while(c=z[i++],d={H:'B',F:'S'}[c]||c,y[d]++,y[c]<3);return d}


Returns S, X or B for a strike-out, on-base or walk.

Auto-formatted version:

f = (z) => {
y = { B: -1, S: (i = 0), F: 0 };
while (((c = z[i++]), (d = { H: 'B', F: 'S' }[c] || c), y[d]++, y[c] < 3));
return d;
};


# Foxrabbit's Finite-State Binary (FSB), 57 bytes

The 57 bytes in hexadecimal are:

01
04 43 04 01
06 47 08 02
06 49 0c 03
06 54 09 0c
00 00 05 05
02 04 06 07
01 00 00 00
02 48 07 0c
01 02 0a 0b
01 00 0a 0a
02 03 0b 0c
00 00 00 00
05 00 0d 0d
07 00 0d 0d


Assumes an ASCII-superset environment. Outputs a single byte with value 3 for strike-out, value 72 (ASCII 'H') for walk, or value 88 (ASCII 'X') for ball in play.

As an FFM program:

Get;inp;67;Ball:NotB
NotB;nop;71;Foul:NotBF
NotBF;nop;73;Print:NotBFH
NotBFH:nop;84;Strike:Print
Ball;lft;0;CountBall:CountBall
CountBall;inc;4;BallRet:SetH
BallRet;rgt;0;Get:Get
SetH;inc;72;SetH:Print
Foul;rgt;2;CountStrike:StrikeRet
Strike;rgt;0;CountStrike:CountStrike
CountStrike;inc;3;StrikeRet:Print
StrikeRet;lft;0;Get:Get
Print;out;0;End:End
End;hlt;0;End:End


The VM for both languages is a finite-state machine with access to an infinite tape of byte values. I used the initial tape cell for the most recent input character, the cell to the right for the number of strikes, and the cell to the left for the number of balls.

Try it online! In that link, the header and footer are the FFB interpreter written in Python and linked from the esolangs page, but modified a little to take an io.BytesIO instead of opening a binary file, and to skip the chr` on an output instruction (since a printed byte 3 is annoying to verify). So the output there will be the decimal string "3", "72", or "88".