g | x | w | all
Bytes Lang Time Link
193Nim250728T111931ZjanAkali
236C GCC230224T171848ZPeter
227C# Visual C# Interactive Compiler200312T140920ZJirka Pi
083Ruby200306T100047ZG B
176Python 3.8200306T114359ZNoodle9
027Japt200306T233950ZShaggy
057Charcoal200306T224739ZNeil
021Jelly200306T195528ZNick Ken
189R200306T181809ZJohn
023Husk200306T203934ZLeo
204Red200306T135134ZGalen Iv
093Perl 5 p MListUtil=product200306T184533ZXcali
026Jelly200306T181953ZJonathan
080JavaScript ES6200306T095334ZArnauld
206SNOBOL4 CSNOBOL4200306T161811ZGiuseppe
155Python 3200306T131425Zovs
02605AB1E200306T101317ZExpired
173Wolfram Language Mathematica200306T104849ZZaMoC
02005AB1E200306T094150ZKevin Cr
070Retina200306T112642ZNeil

Nim, 193 bytes

include math,prelude
proc(a=""):auto=join a.split(' ').mapIt (var b,c=it[0..^2];[it,try:($b.parseInt.fac)except:(for i in 2..b.len:c&=b[0..^i];c),""][int(it[^1]<'"')+int it in ["1!","2!"]])," "

Exception behaviour:

"I have 2! games" -> "I have  games"

Try it in wandbox!

Ungolfed:

proc facJoke(input = ""): auto =
  join(
    input.split(' ').mapIt (
      var word, acc = it[0..^2] # substring (^2 == it.len-2)
      [
        it,   # 1st element - unchanged word
        (
          try:
            $word.parseInt.fac
          except:
            for i in 2..word.len:
              acc &= word[0..^i]
            acc
        ),    # 2nd element - either fac(n) or transformed word if fails to parse integer
        ""    # 3rd element - empty string
      ][ int(it[^1] < '"') + int(it in ["1!","2!"]) ] # indexing array with sum of two bools
                                                      # [a-zA-Z] > '"' > '?'
                                                      # string in [array of strings]
                                                      # int(true) + int(true) == 2
    ),
  " ") # second argument of join

C (GCC), 236 bytes

i,j;char*a,*b,*c,*d;f(char*s){a=strchr(s,33);for(b=a;b>s&!isspace(b[-1]);--b);for(d=s;d-b;)putchar(*d++);*a=0;if(*b&64)for(c=a;*b;*--c=0)printf("%s",b);else{for(i=j=1;j-atoi(b);)i*=++j;printf("%d",i);}printf("%s",a+1);j>0&j<3&&puts(b);}

Try It Online!

Explanation:

i,j;
char*a,*b,*c,*d;

f(char*s)
{
    // a is the first '!' in s
    a=strchr(s,33);
    // b is the first character after the first whitespace before a, or s if there is no whitespace there
    for(b=a;b>s&!isspace(b[-1]);--b);
    // Print each character from s to b
    for(d=s;d-b;)putchar(*d++);
    // Null-terminate s at a
    *a=0;
    // If b is alphabetic
    if(*b&64)
        // While there's anything left of b, print b and shorten it by adding an earlier null terminator
        for(c=a;*b;*--c=0)
            printf("%s",b);
    else {
        // If b is not alphabetic, it's assumed to be a number.
        // i is the product of all j-s between 1, and b as a number
        for(i=j=1;j-atoi(b);)i*=++j;
        printf("%d",i);
    }
    // Print everything after the exclamation mark
    printf("%s",a+1);
    // Print the number twice if it's 1 or 2
    j>0&j<3&&puts(b);
}

ceilingcat's 226 byte version:

i,j;char*a,*b,*c,*d;f(char*s){for(b=a=index(s,33);b>s&!isspace(b[-1]);--b);for(d=s;*a=d-b;)putchar(*d++);if(*b&64)for(c=a;*b;*--c=0)printf(b);else{for(i=j=1;j-atoi(b);)i*=++j;printf("%d",i);}printf("%s",a+1);j>0&j<3&&puts(b);}

Try It Online

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!

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!

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)

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

Charcoal, 57 bytes

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

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

≔⌕θ!η

Find the position of the !.

≔⊟⌕A⁺ …θη ζ

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...

¿ΣεIΠ…·¹Iε

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

⭆ε…ε⁻Lεκ

... 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 !.

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

R, 205 201 194 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!

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)

Red, 242 225 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!

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!

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?
        Ʋ                  -            ...then: last four links as a monad:
 Ṗ                         -              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)
            $              -            ...else: last two links as a monad:
          Ƥ                -              for prefixes:
         Ṗ                 -                pop (remove the `!` from each prefix)
           Ṛ               -              reverse
                   ¹       -   ...else: no-op
                         K - join with spaces
                           - implicit, smashing print

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()

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

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!

05AB1E, 26 22 26 bytes

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

Try it online!

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

05AB1E, 23 22 26 25 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.

Try it online or verify all test cases.

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)

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.