Sometimes I make bad jokes... And a bad joke I like to make involves interpreting exclamation marks in sentences as the factorial sign.

Your task is to write a program that receives a sentence and applies the factorial joke to the sentence.

The "factorial joke" consists of looking for exclamation marks "!" and doing the factorial of whatever is to the left of it. If the thing to the left is an integer, then the usual factorial is used. If the thing to the left is a word (a sequence of characters in [a-zA-Z], delimited by spaces), then we want to concatenate all of the subsequent prefixes of the word.

E.g. if the word was abcd then abcd! = abcdabcaba.

However, there is an exception, which is when the sentence contains a "1!" or "2!", because 1! = 1 and 2! = 2 and the joke doesn't really work. In these cases, your program can do whatever it wants EXCEPT applying the factorial joke or returning the same sentence.

# Input

A sentence with characters in the range [a-zA-Z0-9 !] with the restriction that only one exclamation mark "!" is present and to the right of an integer or a "word". This means you don't have to worry with things as "abc4!" as a word here is defined as a sequence of alphabetical characters delimited by spaces.

# Output

The same sentence, but with the factorial joke applied to the word or integer on the left of the exclamation mark.

# Test cases

You can find a Python reference implementation here

We lost the match 3 to 0! -> We lost the match 3 to 1
ora bolas! -> ora bolasbolabolbob
give me 4! -> give me 24
I wanted 2! give me 2 -> undefined
Let us build a snowman! -> Let us build a snowmansnowmasnowmsnowsnosns
We are late because it is past 17! -> We are late because it is past 355687428096000
I only have one! -> I only have oneono
I only have 1! -> undefined
Incredible! I have never seen anything like it -> IncredibleIncrediblIncredibIncrediIncredIncreIncrIncInI I have never seen anything like it
What an amazing code golf challenge this is! -> What an amazing code golf challenge this isi
So many test cases! -> So many test casescasecascac
These are actually just 11! -> These are actually just 39916800
No wait! there are 13 -> No waitwaiwaw there are 13

This is so shortest submission in bytes, wins! If you liked this challenge, consider upvoting it... And happy golfing!

# 05AB1E, 23222625 20 bytes

ð¡εD'!åi¨ÐaiηRJë2Lså+!]ðý

+4 bytes for two bug-fixes (+1 in case the input is without spaces; +3 for 0!)
-5 bytes thanks to @Grimmy.

Outputs 2/3 for 1!/2! respectively.

Explanation:

ð¡                  # Split the (implicit) input-string on spaces
# (NOTE: # cannot be used here if the input doesn't contain spaces)
ε                 # Map each part to:
D                #  Duplicate it
'!åi           '#  If it contains a "!":
¨           #   Remove the last character (the "!")
D          #   Duplicate it
!         #   Take the factorial of it (strings remain the same)
s        #   Swap to get the duplicate value again
>       #   Increase it by 1 (strings remain the same)
η      #   Get the prefixes of this string/integer
R     #   Reverse it
J    #   And join them together to a single string
M   #   And then push the largest value on the stack
#   (which is either the factorial-integer;
#    or the integer+1 if it was 0, 1, or 2;
#    otherwise it's the longest string, which is the joined prefixes)
]                 # Close both the map and if-statement
ðý               # And join everything with space delimiter again
# (after which the result is output implicitly)
• You can unify the alphabetic case with the 1 or 2 case by testing for y! = y. 22. Mar 6 '20 at 12:54
• Alternative 22, featuring 200% more loops. Mar 6 '20 at 12:55
• 20 (alternative: ð¡εD'!åi¨D!s>ηRJM]ðý). Mar 6 '20 at 13:06
• @Grimmy Nice approach with the > to cover the edge case 0!! I was trying to just use ! and alike on the strings to get rid of the if-statement, since I knew they're be unaffected, but couldn't really figure it out. PS: I've used your alternative 20-byter, since it was closer to my initial solution. Mar 6 '20 at 13:25

# Python 3.8, 222 $$\\cdots\$$ 178 176 bytes

Saved 2 bytes thanks to Kevin Cruijssen!!!
Added 2 3 bytes to fix bugs.
Saved 5 bytes thanks to ovs!!!

lambda j:(g:=re.match(r"(.*)\b(\w+)!(.*)",j).group)(1)+(str(math.perm(int(f)))*-~(f in'1 2')if(f:=g(2))[0]<':'else''.join(f[:i]for i in range(len(f),0,-1)))+g(3)
import math,re

Try it online!

Replaces 1! with 11 and 2! with 22.

### Ungolfed:

import math,re
def f(j):
m=re.match(r"(.*)\b(\w+)!(.*)" ,j)
f=m.group(2)
if f in ('1','2'):f=''
if re.match(r"\d+",f):
f=str(math.factorial(int(f)))
else:
f=''.join(f[:i] for i in range(len(f),0,-1))
return m.group(1)+f+m.group(3)
• Why are the imports in your golfed version behind the lambda while in front of the code in the ungolfed version?
– Mast
Mar 9 '20 at 13:45
• @Mast To golf away the header which assigns a name to the lambda. This is standard practice since you save 2 bytes (f=) and you don't need the imports until the function is actually called (in the test code, for instance). The header is f=\ so the function can be called. Mar 9 '20 at 13:49

# JavaScript (ES6),  86 85  80 bytes

Replaces 1! and 2! with 0 ... ¯\_(ツ)_/¯

s=>s.replace(/\w+!/,g=s=>(s=s.slice(0,-1))?1/s?'1006'[s]||s*g(s-1+'#'):s+g(s):s)

Try it online!

### How?

Because the callback function of replace() is recursive, it's better to feed it with a single variable $$\s\$$ to keep the recursive calls short. That's why the ! is captured along with the word preceding it.

The same slice(0,-1) is used to remove the ! on the first iteration and to build the prefixes in case of a string. In order to make the factorial computation compatible with that method, we just have to pad the recursive argument with a random character which is immediately removed by the next iteration.

To deal with the weird edge cases $$\1!\$$ and $$\2!\$$, we use the small lookup string '1006' to stop the recursion whenever the value is less than or equal to $$\3\$$. This way, we have $$\0!=1\$$, $$\3!=6\$$ and $$\n! = 6\cdot\prod_{k=4}^{n}k\$$ for $$\n>3\$$, but $$\1!=2!=0\$$ (sic).

### Commented

NB: alternate slash symbols used in the regular expression to prevent the syntax highlighting from being broken

s =>                        // s = input string
s.replace(                // find in s
⁄\w+!⁄,                 // a word followed by a '!'
g = s =>                // g is a recursive function computing the replacement
(s = s.slice(0, -1))  //   remove the last character from s
//   (for the 1st iteration, it removes the '!')
?                     //   if the resulting string is not empty:
1 / s ?             //     if this is a numeric value:
'1006'[s]         //       0 -> 1, 1 -> 0, 2 -> 0, 3 -> 6
|| s *            //       for all other values, multiply s by
g(s - 1 + '#') //       the result of a recursive call with s - 1,
//       padded with a '#' for the next slice()
:                   //     else (s is a string):
s +               //       append s
g(s)              //       append the result of a recursive call
:                     //   else (s is empty):
s                   //     stop recursion
)                         // end of replace()
• How about 9 instead of -~s? Mar 6 '20 at 9:59
• @user41805 -~s also turns $0$ into $1$. Mar 6 '20 at 10:02

# Python 3, 157 155 bytes

Thanks to Noodle9 for -2 bytes.

(*s,w),b=map(str.split,input().split('!'))
w*=1+(w in'1 2')
if'A'>w:
i=int(w);w=1
while i:w*=i;i-=1
else:
d=w[:-1]
while d:w+=d;d=d[:-1]
print(*s,w,*b)

Try it online!

• -3 bytes by removing the w*=1+(w in'1 2') and changing the i=int(w) to i=int(w*-~(w in'1 2')) and an additional -5 bytes by changing the d=w[:-1] and while d:w+=d;d=d[:-1] to d=w and while d:d=d[:-1];w+=d: 147 bytes. Mar 12 '20 at 15:07
• Actually, i=int(w)+(w in'1 2') is an additional -2: 145 bytes Mar 12 '20 at 15:33

# Wolfram Language (Mathematica), 173 bytes

StringRiffle[""<>If[Last@#=="!",If[NumberQ[n=ToExpression[""<>#]],If[n>0&&n<3&&First@#!="0","",ToString@n],Rest@NestList[Most,#,Length@#-1]],#]&/@Characters/@StringSplit@#]&

Try it online!

-2 bytes from @Kevin Cruijssen

• Can you change the n==1||n==2 to n>0&&n<3 for -2 bytes, or would there be a possible edge case I'm overlooking preventing this? Mar 6 '20 at 11:41
• Your solution doesn't work for 0!
– RGS
Mar 6 '20 at 13:21
• @RGS fixed . . . Mar 6 '20 at 14:08

# Perl 5-p -MList::Util=product, 93 bytes

s/(\d+)!/$1?$1>2?product 1..$1:'':1/e;s%(\w+)!%join'',map{substr$1,0,$_}reverse 1..length$1%e

Try it online!

• This is based on your answer, but just 87 bytes: sub f{pop=~s/(\d+)!/$1?$1>2?product 1..$1:'':1/er=~s/(\S*)(.)!/$1?$1.$2.f("$1!"):$2/er} but it doesn't work in all perl versions. It works in 5.22 and 5.26, but not 5.28 and 5.30. Don't know why. Mar 7 '20 at 18:50

# Retina, 70 bytes

' %/!$/&/^[012]!$/(..
$.(*__ /\D./(^.$
.+
*
_
$.>$*
~^
^.$*¶$$.( Try it online! Link includes test cases. Explanation: ' % Split the input into words. /!/& Only process words that end in !. /^[012]!/( If this is 0!, 1! or 2!... .. .(*__ ... then return the incremented digit. (Approach shamelessly stolen from @Arnauld.) /\D./( If the word contains a non-digit (excluding the trailing !)... ^.  ... then replace it with all of its prefixes in reverse order. (Note that these are exclusive prefixes so that the empty prefix is included but the string including the ! is excluded.) .+ * _ .>* ~^ ^.*n$$.( Otherwise compute the factorial. • This seems to fail for inputs containing 1! or 2! due to the rule "In these cases, your program can do whatever it wants EXCEPT applying the factorial joke or returning the same sentence." Mar 6 '20 at 11:38 • @KevinCruijssen Ah, I overlooked that part of the rule. Let me think about how I can apply that rule in the fewest amount of bytes. – Neil Mar 6 '20 at 12:03 • @KevinCruijssen Well, that cost me 11 bytes, but I was able to remove the 0! special case for 3 bytes and I found another byte I'd forgotten to golf off, so it could have been worse... – Neil Mar 6 '20 at 13:23 # SNOBOL4 (CSNOBOL4), 206 bytes D =84 ** 9 INPUT ARB . L SPAN(&UCASE &LCASE D) . W '!' REM . R W SPAN(D) :F(S) N =W + 1 LT(W,3) :S(O) N =W F W =W - 1 GT(W,1) :F(O) N =N * W :(F) S N =N W W ARB . W RPOS(1) :S(S) O OUTPUT =L N R END Try it online! 84 ** 9 = 208215748530929664 which has all the digits from 0-9. D =84 ** 9 ;* D contains every decimal digit. INPUT ARB . L SPAN(&UCASE &LCASE D) . W '!' REM . R ;* split the input into an ARBitrary Left part, the Word followed by a '!' and the REMainder to the Right part. W SPAN(D) :F(S) ;* if W has digits, goto S N =W + 1 LT(W,3) :S(O) ;* if W < 3, then N = W + 1 and goto O N =W ;* set N = W F W =W - 1 GT(W,1) :F(O) ;* decrement W, and goto O if W == 0 N =N * W :(F) ;* N = N * W, goto F S N =N W ;* W is a string, so set N = N W W ARB . W RPOS(1) :S(S) ;* set W to W excluding its final character and if W had any characters, goto S O OUTPUT =L N R ;* output Left, the New middle, and the Right string. END # Red, 242225 204 bytes func[s][c: charset[#"0"-#"z"]parse s[any[change[copy t any c"!"]( case[find"12"t[0]t <"A"[t: to 1 t p: 1 while[t > 1][p: p * t t: t - 1]p]t >":"[p: copy t until[take/last p append t p p =""]t]])| skip]]s] Try it online! # R, 205201194 189 bytes s=scan(,'');l=grepl('\\w!',s);w=s[l];n=nchar;k=function(m,x)substr(x,1,n(x)-m);w=k(1,w);b=as.numeric(w);o=if(is.na(b),Reduce(paste0,sapply(0:n(w)-1,k,w)),gamma(b+1));s[l]=o;if(o!=w)cat(s) Try it online! # Jelly, 22 21 bytes ;”ḟV$¹ƤṖṚƊfØD$?ċ¡€”!K Try it online! A full program taking a list of words and printing a string. For the 1 and 2 cases the 1 and 2 are dropped from the output. Saved a byte now that a list of words is permitted input. ## Explanation Ḳ | Split at spaces ċ¡€”! | For each word, if it contains a ! then do the following: fØD$?       | - If any characters are left after restricting to digits:
$| - Then: ;”ḟ | - Append ḟ (which will filter out 1 and 2 at the next step because x! == x) V | - Evaluate (i.e. calculate the factorial and then filter out the input number) Ɗ | - Else: ¹Ƥ | - Prefixes Ṗ | - Remove last Ṛ | - Reverse K | Finally, join with spaces # Ruby, 88 85 83 bytes ->s{s.sub(/\S+!/){|z|z<?1?1:z>?9?z.chars.map{z=z.chop}*'':eval([6,*4..z.to_i]*?*)}} Try it online! # Jelly, 26 bytes ḲṖVN!Ƒ¡!ƲṖƤṚ$<”AẠ$?¹ċ?€”!K A full program accepting a string which prints the result. Try it online! Kind of a tough one for Jelly. ### How? ḲṖVN!Ƒ¡!ƲṖƤṚ$<”AẠ$?¹ċ?€”!K - Link: list of characters, s Ḳ - split at space characters -> words ”! - literal '!' character € - for each word: ? - if... ċ - ...condition: count (i.e. contains?) ? - ...then: if...$         -            ...condition: last two links as a monad:
<             -              less than:
”A           -                literal 'A' character
Ạ          -              all?
Ṗ                         -              pop (remove the '!')
V                        -              evaluate as Jelly code (get an integer)
¡                    -              repeat...
Ƒ                     -              ...number of times: is invariant under?:
!                      -                factorial
N                       -              ...action: negate (i.e. 1;2;X -> -1;-2;X)
!                   -              factorial (-1! = -2! = inf)
Ƥ                -              for prefixes:
Ṗ                 -                pop (remove the ! from each prefix)
Ṛ               -              reverse
¹       -   ...else: no-op
K - join with spaces
- implicit, smashing print

# Husk, 23 bytes

wm??öΣ↔ḣhȯs§Y→ΠiV√I€'!w

Try it online!

Maps 1 to 2 and 2 to 3.

On my phone, so writing an explanation is not too easy, but I'll try.

Code with parentheses for "clarity":

wm(?(?(Σ↔ḣh)(s§Y→Πi)(V√))(I)(€'!))w

The main part of the code is built by conditionals, they may be a bit easier to read once you know that in Husk ?abc means if c then a else b. Also, most of the time fg is the composition of functions f and g, so we are actually applying g before f (to oversimplify it, you should read the code backwards)

A pseudocode for this could be:

Split input into words, map the following function to each word and then join them again.
If word contains '!':
if any character of the word is a letter:
get the head of the word (drop final '!')
get the list of prefixes (heads)
reverse it
join prefixes in a single word
else:
convert word to integer
get the maximum between:
successor
factorial
convert it back to string
else apply Identity function (do nothing)

# Charcoal, 57 bytes

≔⌕θ!η≔⊟⌕Ａ⁺ …θη ζ…θζ≔✂θζη¹ε≡ε2¹1¹0¦1¿ΣεＩΠ…·¹Ｉε⭆ε…ε⁻Ｌεκ✂θ⊕η

Try it online! Link is to verbose version of code. Explanation:

≔⌕θ!η

Find the position of the !.

≔⊟⌕Ａ⁺ …θη ζ

Find the position of the start of the word ending in !.

…θζ

Print the prefix of the input before the word.

≔✂θζη¹ε≡ε

Extract the word and switch on it.

2¹1¹0¦1

If it's 2 or 1 then output a -, otherwise if it's a 0 then outputs 1, otherwise...

¿ΣεＩΠ…·¹Ｉε

... if it's an integer then output its factorial, otherwise...

⭆ε…ε⁻Ｌεκ

... output its prefixes in reverse order. (These are inclusive prefixes, so they include the word itself but not the empty string.)

✂θ⊕η

Print the suffix of the input after the !.

• @RGS What part of "If it's 2 or 1 then output a -" computes the factorial of 1?
– Neil
Mar 7 '20 at 10:47
• I was sure I had tested your code with 0! and 1! and it gave 1 in both cases... But now I tested it again and it does output - for 1!, so something weird happened!
– RGS
Mar 7 '20 at 10:59

# Japt, 27 bytes

Well, this is just god-awful but I've spent far too long on it to not post it. Kids, let this be a warning against trying to golf without your full faculties about you!

r"(%w+)!"Ï<3?°Y:Yn ÊªYå+ Ôq

Try it

# 05AB1E, 2622 26 bytes

ð¡εD'!¢i¨D.ïiÐ!Ês!*ëηRJ]ðý

Try it online!

• Nice alternative, but umm.. it's pretty ironic you've asked me about 0! even though your own approach doesn't work for it either. ;) Mar 6 '20 at 10:15
• @KevinCruijssen Yes it does? 0! is 1 Mar 6 '20 at 10:16
• 0! is 0?.. There are also a few other problems btw: # doesn't work for inputs without spaces (i.e. test results in t e s right now - also missing a trailing t :S); and the trailing 2 gets removed in test case I wanted 2! give me 2 Mar 6 '20 at 10:18
• Will fix just got 4 bytes back :) Mar 6 '20 at 10:20
• Based on the Á it seems you're currently assuming the ! is always at the end? That's probably where most of the issues I mentioned come from. Oh, and # to ð¡ fixes the issue of input without spaces. :) Had that initially wrong as well in my answer. Mar 6 '20 at 10:22

# C# (Visual C# Interactive Compiler), 227 bytes

s=>{var w=s.Split("!")[0].Split(" ").Last();if(!int.TryParse(w,out int n))for(;(w=w.Substring(1)).Length!=0;)s=s.Replace("!",w+"!");else{for(int i=n;--i>0;n=n*i);s=s.Replace(w+"!",""+(n<1?1:n>4?n:0));}return s.Replace("!","");}

Try it online!

• Suggest n*=i istead of n=n*i Aug 15 '20 at 6:22