Programming is very rigid. You can't tell a program to "output the banana count", you have to tell it to print(bananas).

But when you do that, you end up with a problem: you don't know how many bananas you have beforehand, so you don't know whether to use a plural.

Sometimes, programmers go the lazy way. Instead of checking, they just print there are X banana(s).

But that's ugly, so we need a program to fix this.

The method(s)

To remove the ambiguous plurals in a string, follow the following steps:

  1. Split the string on spaces into a list of words.

  2. For every word that ends with (s), do the following:

    • If the preceding word is a, an, 1 or one, remove the (s) at the end of the word.
    • Otherwise, if the word is the first word in the string or the preceding word is not a, an, 1 or one, replace the (s) at the end of the word with s.
  3. Join the list of words back together into a string, preserving the original whitespace.


Let's take a string there's a banana(s) and three apple(s).

First, we split the string into a list of words: ["there's", "a", "banana(s)", "and", "three", "apple(s)"]

For the second step, we take the two words ending with (s): banana(s) and apple(s).

The word before banana(s) is a, so we remove the (s), making it banana. The word before apple(s) is three, so we change the (s) to s, thus it becomes apples.

We now have ["there's", "a", "banana", "and", "three", "apples"]. Joining the list back together, we get there's a banana and three apples. This is our end result.

The challenge(s)

Create a program or function that takes an ambiguous string in any reasonable format and returns the un-ambiguated version of that string.

You may assume the string contains no newlines, tabs or carriage returns.

I forgot to specify whether to split on groups of spaces or spaces (i.e. whether okay then with two spaces should be ["okay", "then"] or ["okay", "", "then"]) when posting the challenge, so you may assume either form of splitting.

Test case(s)

Input                                         -> Output
there are two banana(s) and one leprechaun(s) -> there are two bananas and one leprechaun
there's a banana(s) and three apple(s)        -> there's a banana and three apples
apple(s)                                      -> apples
one apple(s)                                  -> one apple
1 banana(s)                                   -> 1 banana
banana                                        -> banana
preserve    original      whitespace(s)       -> preserve    original      whitespaces
11 banana(s)                                  -> 11 bananas
an apple(s)                                   -> an apple
this is a te(s)t                              -> this is a te(s)t
I am a (s)tranger(s)                          -> I am a (s)tranger


As this is , the submission with the least bytes wins!

  • \$\begingroup\$ This question has been sandboxed. \$\endgroup\$
    – LyricLy
    Sep 30, 2017 at 23:16
  • \$\begingroup\$ Should the apple(s) test case yield apples instead? The challenge states Otherwise, if the word is the first word in the string . . . replace the (s) at the end of the word with s. I note that this case yielded apples in sandbox for the first three revisions but changed at the fourth. \$\endgroup\$ Oct 1, 2017 at 0:02
  • \$\begingroup\$ @fireflame241 When writing the second draft of the rules, I was going to make it so that the start of the string is unchanged. I changed that rule later, but not the test case. Good catch. \$\endgroup\$
    – LyricLy
    Oct 1, 2017 at 0:03
  • \$\begingroup\$ Test case suggestion: There's a single banana(s) -> There's a single bananas. \$\endgroup\$ Oct 1, 2017 at 0:17
  • 1
    \$\begingroup\$ @JonathanAllan You cannot. I'll add a few test cases. \$\endgroup\$
    – LyricLy
    Oct 1, 2017 at 0:24

12 Answers 12


Mathematica, 151 148 bytes

StringReplace[j=" ";k=Except@j;j<>j<>#<>j,j~~a:k...~~s:j..~~w:k..~~"(s)"~~j:>{j,a,s,w,If[FreeQ[a,"a"|"an"|"1"|"one"],"s",""]}<>j]~StringTake~{3,-2}&


j=" ";k=Except@j

Set j to a whitespace character(s). Set k to the pattern "not j" (= non-whitespace character).


Prepend two whitespace(s) and append one whitespace(s) to the input.


For a substring(s) matching the pattern:

  1. One whitespace(s), followed by
  2. a length-zero or longer substring consisting of only non-whitespace character(s) (quantifier) (call this a), followed by
  3. a length-one or longer substring consisting of only whitespace character(s) (call this s), followed by
  4. a length-one or longer substring consisting of only non-whitespace character(s) (word) (call this w), followed by
  5. the string "(s)", followed by
  6. a whitespace(s)

If a is not one of the singular word(s), evaluate to "s", otherwise "".

StringReplace[..., ... :>{j,a,s,w,If[FreeQ[a,"a"|"an"|"1"|"one"],"s",""]}<>j]

Replace the matching pattern with j, a, s, w, If[FreeQ[a,"a"|"an"|"1"|"one"],"s",""], and j joined together.

... ~StringTake~{3,-2}

Take from position 3 to position -2 (1-indexed; negative indices count from the end). This is because we added three space(s) in the beginning.

  • 3
    \$\begingroup\$ Why not use the builtin for removing plural-S? \$\endgroup\$ Oct 1, 2017 at 16:23

Python 3, 94 bytes

lambda s,r=re.sub:r(r"\(s\)( |$)","s",r(r"\b(an?|1|one)(\s+)(.+)\(s\)",r"\1\2\3",s))
import re

Try it online!

-4 bytes thanks to i cri everytim (I think this is acceptable)

  • \$\begingroup\$ @JonathanAllan Fixed, thanks. \$\endgroup\$
    – hyper-neutrino
    Oct 1, 2017 at 1:03
  • 1
    \$\begingroup\$ __import__ can't possibly be shorter... Yup, it's 4 bytes shorter as a regular import re. \$\endgroup\$ Oct 1, 2017 at 1:16
  • \$\begingroup\$ @icrieverytim huh you're right (only 3 bytes though) thanks \$\endgroup\$
    – hyper-neutrino
    Oct 1, 2017 at 1:40
  • \$\begingroup\$ wait no here's 94 \$\endgroup\$ Oct 1, 2017 at 1:47
  • \$\begingroup\$ @icrieverytim ._. oh nice. thanks! \$\endgroup\$
    – hyper-neutrino
    Oct 1, 2017 at 1:49

Retina, 53 bytes

(( |^)(a|an|1|one) [^ ]*)\(s\)( |$)
\(s\)( |$)

Try it online!

  • 1
    \$\begingroup\$ This deletes the space after banana(s) in there's a banana(s) and three apple(s) - try this fix \$\endgroup\$
    – Neil
    Oct 1, 2017 at 9:29
  • \$\begingroup\$ You can change a|an to an? for -1 byte \$\endgroup\$
    – PunPun1000
    Oct 2, 2017 at 16:18

Mathematica, 313 bytes

(Table[If[StringLength@z[[i]]>3&&StringTake[z[[i]],-3]=="(s)",z[[i]]=StringDrop[z[[i]],-3];t=1;While[z[[i-t]]=="",t++];If[FreeQ[{"a","an","1","one"},z[[i-t]]],z[[i]]=z[[i]]<>"s"]],{i,2,Length[z=StringSplit[#," "]]}];If[StringTake[z[[1]],-3]=="(s)",z[[1]]=StringDrop[z[[1]],-3];z[[1]]=z[[1]]<>"s"];StringRiffle@z)&

Perl 5, 43 + 1 (-p) = 44 bytes

s/\b((one|1|an?) +)?\S+\K\(s\)\B/"s"x!$1/ge

Match every (s) at end of word, replace it with !$1 (1 or 0) esses.


Pyth - 53 bytes

Follows the algorithm pretty much as it is.


Try it online here.

  • 1
    \$\begingroup\$ Fails on there are two banana(s) and one leprechaun(s) (two spaces after the one). The original whitespace is preserved, but leprechaun(s) ignores the one before it. \$\endgroup\$
    – LyricLy
    Sep 30, 2017 at 23:44
  • 1
    \$\begingroup\$ @LyricLy you have not explicitly stated this in the OP. With two spaces (using (1) of your "method(s)" section of "split the string on spaces into a list of words") there is actually an empty word between one and leprechaun(s) \$\endgroup\$ Oct 1, 2017 at 0:07

Jelly,  52 51  49 bytes

Jelly has not got one regex(s) atom


A full program accepting a string (using Python formatting if multiline or containing quotes) and printing the output.

Try it online! or see the test-suite.


Ṫ - Link 1, tail: two words (list of lists)
Ṫ - tail

Ñ;”s - Link 2, tail and replace last three chars with an 's': two words (list of lists)
Ñ    - call the next link (3) as a monad
  ”s - literal 's'
 ;   - concatenate

Ṫḣ-3 - Link 3, tail and remove the last three chars: two words (list of lists)
Ṫ    - tail
  -3 - literal minus three
 ḣ   - head from index (1-indexed and modular)

UṪw“)s(”⁼1 - Link 4, tail ends with "(s)"?: two words (list of lists)
U          - upend (reverse each word)
 Ṫ         - tail
   “)s(”   - literal [')', 's', '('] - that is "(s)" reversed
  w        - index of first sublist equal to that or 0 if not found
         1 - literal one
        ⁼  - equal?

“µḣ⁴µuʠg*»ḲċḢ‘×Ç‘ - Link 5, categorise: two words (list of lists)
“µḣ⁴µuʠg*»        - compression of string "a 1" + word " an" + word " one"
          Ḳ       - split on spaces = ["a", "1", "an", "one"]
            Ḣ     - head (the first word)
           ċ      - count occurrences (of head in the list - either 0 or 1)
             ‘    - increment
               Ç  - call the last link (4) as a monad - i.e. f(two words)
              ×   - multiply
                ‘ - increment - so we have: 1 for ["1", "blah"],
                  -             2 for ["blah", "blah(s)"] or 3 for ["1", "blah(s)"]

⁶;ḲÇĿ2ƤK - Main link: list of characters, the string
⁶        - literal space character
 ;       - concatenate (place a space at the beginning as we want to inspect pairs)
  Ḳ      - split on spaces (giving an empty list at the start)
     2Ƥ  - for all infixes of length two:
    Ŀ    -   call the link at the given index as a monad:
   Ç     -     call the last link (5) as a monad
       K - join the result with spaces
         - implicit print
  • \$\begingroup\$ I'm curious as to why you used as a separate link. Does this prevent from deleting the element from the original list? \$\endgroup\$
    – hyper-neutrino
    Oct 1, 2017 at 2:48
  • \$\begingroup\$ No, I need to get the tail of the pair... writing up a code commentary, maybe you can spot a golf once you see that. \$\endgroup\$ Oct 1, 2017 at 2:52
  • \$\begingroup\$ Ah okay. Thanks, I'll try to spot the golf once there's a commentary (or before then)! \$\endgroup\$
    – hyper-neutrino
    Oct 1, 2017 at 2:55
  • \$\begingroup\$ So links 1, 2 and 3 all tail, and link 5 chooses which to call and uses Ŀ to do so, but I don't see a short way to tail inside link 4, but there might be. There could even be a way to get the tail of link 4 in there too! \$\endgroup\$ Oct 1, 2017 at 3:08
  • \$\begingroup\$ @HyperNeutrino I think that the Ŀ thingy may call the first link, that's why is a link on its own. \$\endgroup\$ Oct 1, 2017 at 7:43

Java (OpenJDK 8), 91 83 96 bytes

s->s.replaceAll("((^| )(an?|1|one) +\\S+)\\(s\\)(?= |$)","$1").replaceAll("\\(s\\)(?= |$)","s");

Try it online!


Perl 5, 56 + 1 (-p) = 57 bytes

s/\b(an?|1|one) +\S+\K\(s\)(?= |$)//g;s/\(s\)( |$)/s$1/g

Try it online!

  • 1
    \$\begingroup\$ Not on the test cases, but I think this fails for a hel(s)lo. \$\endgroup\$
    – Neil
    Oct 1, 2017 at 9:34
  • \$\begingroup\$ That's working properly as provided in the test case. It's near the bottom of the test cases in my TIO link. \$\endgroup\$
    – Xcali
    Oct 1, 2017 at 21:37
  • \$\begingroup\$ Well I'll just have to get a hel(s)lo added to the test cases, and then maybe you'll fix your code... \$\endgroup\$
    – Neil
    Oct 2, 2017 at 0:06

JavaScript (ES6), 88 87 bytes

a=>a.replace(/(\S+)( +)(\S+)\(s\)/g,(m,f,s,w)=>f+s+w+(/^(a|an|1|one)$/.exec(f)?'':'s'))

Explanation coming soon.

  • 1
    \$\begingroup\$ you can replace \s with ` ` according to "You may assume the string contains no newlines, tabs or carriage returns." \$\endgroup\$ Oct 1, 2017 at 17:43
  • \$\begingroup\$ Fails on "this is a te(s)t". You can fix by adding (\s|$) to the end of the regex. \$\endgroup\$
    – Birjolaxew
    Oct 2, 2017 at 10:36
  • \$\begingroup\$ Also fails on "apple(s)". Fixed in this TIO \$\endgroup\$
    – Birjolaxew
    Oct 2, 2017 at 10:45
  • \$\begingroup\$ Thanks @Birjolaxew, will edit in the changes when I can... \$\endgroup\$
    – user62802
    Oct 2, 2017 at 12:02

JavaScript (ES6), 84 bytes

s=>s.replace(/((^|\S+ +)\S+)\(s\)(?!\S)/g,(_,a)=>a+(/^(1|an?|one) /.test(a)?'':'s'))

Here's an interesting way to rearrange the last part, which is sadly 2 bytes longer:

s=>s.replace(/((^|\S+ +)\S+)\(s\)(?!\S)/g,(_,a)=>a+'s'.slice(/^(1|an?|one) /.test(a)))

JavaScript (SpiderMonkey), 82 bytes

s=s.replace(/(\S+ +(\S+))\(s\)\B/g,(_,a)=>a+("s"[+/^(1|one|an?)\b/i.test(a)]||""))

Try it online!

78 Byte version (less robust)

s=s.replace(/(\S+ +(\S*))\(s\)/g,(_,a)=>a+("s"[+/^(1|one|an?)/i.test(a)]||""))

This is a modified version of ETHproductions' (I don't have 50 rep.)


  • /(\S+ +(\S+))\(s\)/g - the actual pattern to look for (amount object(s))
  • (_,a)=>a - _ is a catch all variable, a is the (\S+ +(\S+))
  • "s"[+/^(1|one|an?)/i.test(a)]||"" - instead of slicing the array, just make a dummy array and get the index (+/.../.test returns a number)
    • should "s"[+/^(1|one|an?)/i.test(a)] return undefined (true, or 1 for the test) return ""

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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