33
\$\begingroup\$

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

Task

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!

\$\endgroup\$

18 Answers 18

9
\$\begingroup\$

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)
|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ You can unify the alphabetic case with the 1 or 2 case by testing for y! = y. 22. \$\endgroup\$ – Grimmy Mar 6 at 12:54
  • \$\begingroup\$ Alternative 22, featuring 200% more loops. \$\endgroup\$ – Grimmy Mar 6 at 12:55
  • 1
    \$\begingroup\$ 20 (alternative: ð¡εD'!åi¨D!s>ηRJM]ðý). \$\endgroup\$ – Grimmy Mar 6 at 13:06
  • \$\begingroup\$ @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. \$\endgroup\$ – Kevin Cruijssen Mar 6 at 13:25
8
\$\begingroup\$

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()
|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ How about 9 instead of -~s? \$\endgroup\$ – user41805 Mar 6 at 9:59
  • 2
    \$\begingroup\$ @user41805 -~s also turns \$0\$ into \$1\$. \$\endgroup\$ – Arnauld Mar 6 at 10:02
8
\$\begingroup\$

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)
|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ Why are the imports in your golfed version behind the lambda while in front of the code in the ungolfed version? \$\endgroup\$ – Mast Mar 9 at 13:45
  • \$\begingroup\$ @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. \$\endgroup\$ – Noodle9 Mar 9 at 13:49
6
\$\begingroup\$

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!

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ -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. \$\endgroup\$ – Kevin Cruijssen Mar 12 at 15:07
  • \$\begingroup\$ Actually, i=int(w)+(w in'1 2') is an additional -2: 145 bytes \$\endgroup\$ – Kevin Cruijssen Mar 12 at 15:33
4
\$\begingroup\$

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

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ 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? \$\endgroup\$ – Kevin Cruijssen Mar 6 at 11:41
  • \$\begingroup\$ Your solution doesn't work for 0! \$\endgroup\$ – RGS Mar 6 at 13:21
  • \$\begingroup\$ @RGS fixed . . . \$\endgroup\$ – J42161217 Mar 6 at 14:08
4
\$\begingroup\$

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!

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ 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. \$\endgroup\$ – Kjetil S. Mar 7 at 18:50
4
\$\begingroup\$

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!

|improve this answer|||||
\$\endgroup\$
3
\$\begingroup\$

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.

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ 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." \$\endgroup\$ – Kevin Cruijssen Mar 6 at 11:38
  • \$\begingroup\$ @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. \$\endgroup\$ – Neil Mar 6 at 12:03
  • \$\begingroup\$ @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... \$\endgroup\$ – Neil Mar 6 at 13:23
3
\$\begingroup\$

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
|improve this answer|||||
\$\endgroup\$
3
\$\begingroup\$

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!

|improve this answer|||||
\$\endgroup\$
3
\$\begingroup\$

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
|improve this answer|||||
\$\endgroup\$
2
\$\begingroup\$

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!

|improve this answer|||||
\$\endgroup\$
1
\$\begingroup\$

05AB1E, 26 22 26 bytes

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

Try it online!

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ 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. ;) \$\endgroup\$ – Kevin Cruijssen Mar 6 at 10:15
  • \$\begingroup\$ @KevinCruijssen Yes it does? 0! is 1 \$\endgroup\$ – Expired Data Mar 6 at 10:16
  • \$\begingroup\$ 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 \$\endgroup\$ – Kevin Cruijssen Mar 6 at 10:18
  • 1
    \$\begingroup\$ Will fix just got 4 bytes back :) \$\endgroup\$ – Expired Data Mar 6 at 10:20
  • \$\begingroup\$ 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. \$\endgroup\$ – Kevin Cruijssen Mar 6 at 10:22
1
\$\begingroup\$

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
|improve this answer|||||
\$\endgroup\$
1
\$\begingroup\$

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)
|improve this answer|||||
\$\endgroup\$
1
\$\begingroup\$

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

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ @RGS What part of "If it's 2 or 1 then output a -" computes the factorial of 1? \$\endgroup\$ – Neil Mar 7 at 10:47
  • \$\begingroup\$ 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! \$\endgroup\$ – RGS Mar 7 at 10:59
1
\$\begingroup\$

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

|improve this answer|||||
\$\endgroup\$
0
\$\begingroup\$

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!

|improve this answer|||||
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.