21
\$\begingroup\$

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.

Example(s)

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

Scoring

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

\$\endgroup\$
  • \$\begingroup\$ This question has been sandboxed. \$\endgroup\$ – LyricLy Sep 30 '17 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\$ – fireflame241 Oct 1 '17 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 '17 at 0:03
  • \$\begingroup\$ Test case suggestion: There's a single banana(s) -> There's a single bananas. \$\endgroup\$ – Jonathan Allan Oct 1 '17 at 0:17
  • 1
    \$\begingroup\$ @JonathanAllan You cannot. I'll add a few test cases. \$\endgroup\$ – LyricLy Oct 1 '17 at 0:24

12 Answers 12

6
\$\begingroup\$

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}&

Explanation

j=" ";k=Except@j

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

j<>j<>#<>j

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

j~~a:k...~~s:j..~~w:k..~~"(s)"~~j

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[FreeQ[a,"a"|"an"|"1"|"one"],"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.

\$\endgroup\$
  • 3
    \$\begingroup\$ Why not use the builtin for removing plural-S? \$\endgroup\$ – Thomas Weller Oct 1 '17 at 16:23
5
\$\begingroup\$

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)

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

Retina, 53 bytes

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

Try it online!

\$\endgroup\$
  • 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 '17 at 9:29
  • \$\begingroup\$ You can change a|an to an? for -1 byte \$\endgroup\$ – PunPun1000 Oct 2 '17 at 16:18
4
\$\begingroup\$

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)&
\$\endgroup\$
3
\$\begingroup\$

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.

\$\endgroup\$
2
\$\begingroup\$

Pyth - 53 bytes

Follows the algorithm pretty much as it is.

K+kczdjdt.e?q"(s)"gb_2+<b_3*\s!}@Ktk[\a"an""one"\1)bK

Try it online here.

\$\endgroup\$
  • 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 '17 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\$ – Jonathan Allan Oct 1 '17 at 0:07
2
\$\begingroup\$

Jelly,  52 51  49 bytes

Jelly has not got one regex(s) atom

Ṫ
Ñ;”s
Ṫḣ-3
UṪw“)s(”⁼1
“µḣ⁴µuʠg*»ḲċḢ‘×Ç‘
⁶;ḲÇĿ2ƤK

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.

How?

Ṫ - 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
\$\endgroup\$
  • \$\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\$ – HyperNeutrino Oct 1 '17 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\$ – Jonathan Allan Oct 1 '17 at 2:52
  • \$\begingroup\$ Ah okay. Thanks, I'll try to spot the golf once there's a commentary (or before then)! \$\endgroup\$ – HyperNeutrino Oct 1 '17 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\$ – Jonathan Allan Oct 1 '17 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\$ – Erik the Outgolfer Oct 1 '17 at 7:43
2
\$\begingroup\$

Java (OpenJDK 8), 91 83 96 bytes

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

Try it online!

\$\endgroup\$
1
\$\begingroup\$

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

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

Try it online!

\$\endgroup\$
  • 1
    \$\begingroup\$ Not on the test cases, but I think this fails for a hel(s)lo. \$\endgroup\$ – Neil Oct 1 '17 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 '17 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 '17 at 0:06
0
\$\begingroup\$

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.

\$\endgroup\$
  • 1
    \$\begingroup\$ you can replace \s with ` ` according to "You may assume the string contains no newlines, tabs or carriage returns." \$\endgroup\$ – SuperStormer Oct 1 '17 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 '17 at 10:36
  • \$\begingroup\$ Also fails on "apple(s)". Fixed in this TIO \$\endgroup\$ – Birjolaxew Oct 2 '17 at 10:45
  • \$\begingroup\$ Thanks @Birjolaxew, will edit in the changes when I can... \$\endgroup\$ – XavCo7 Oct 2 '17 at 12:02
0
\$\begingroup\$

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)))
\$\endgroup\$
0
\$\begingroup\$

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

Explanation

  • /(\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 ""
\$\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.