18
\$\begingroup\$

Challenge

The goal of this challenge is to make a function that takes an input string, a start keyword and a end keyword. The output extracted result is from (but excluded) the given start keyword to (but excluded) end keyword. The output sub-string follows the rules as below.

  • In all cases, the leading/trailing spaces in output sub-string should be removed.

  • If the given start keyword is an empty string, it means that the anchor is at the start of the input string. Otherwise, the first occurrence of the given start keyword is an start anchor. If there is no any occurrence of the given start keyword, the output is an empty string.

  • If the given end keyword is an empty string, it means that the anchor is at the end of the input string. Otherwise, the first occurrence of the given end keyword is an end anchor. If there is no any occurrence of the given end keyword, the output is an empty string.

  • If the location of start anchor is after than the location of end anchor, or a part of the first occurrence of the given start keyword and a part of the first occurrence of the given end keyword are overlapped, the output is an empty string.

Similar but different from Extract a string from a given string, the given start and end anchors are multiple characters.

Here's an ungolfed reference implementation in C#

private static string GetTargetString(string stringInput, string startKeywordInput, string endKeywordInput)
{
    int startIndex;
    if (String.IsNullOrEmpty(startKeywordInput))
    {
        startIndex = 0;
    }
    else 
    {
        if (stringInput.IndexOf(startKeywordInput) >= 0)
        {
            startIndex = stringInput.IndexOf(startKeywordInput) + startKeywordInput.Length;
        }
        else
        {
            return "";
        }
        
    }

    int endIndex;
    if (String.IsNullOrEmpty(endKeywordInput))
    {
        endIndex = stringInput.Length;
    }
    else
    {
        if (stringInput.IndexOf(endKeywordInput) > startIndex)
        {
            endIndex = stringInput.IndexOf(endKeywordInput);
        }
        else
        {
            return "";
        }
    }
    
    
    //    Check startIndex and endIndex
    if (startIndex < 0 || endIndex < 0 || startIndex >= endIndex)
    {
        return "";
    }

    if (endIndex.Equals(0).Equals(true))
    {
        endIndex = stringInput.Length;
    }
    int TargetStringLength = endIndex - startIndex;
    return stringInput.Substring(startIndex, TargetStringLength).Trim();
}

Example Input and Output

The example input and output is listed as below.

Input String Start Keyword End Keyword Output
"C# was developed around 2000 by Microsoft as part of its .NET initiative" ""(empty string) ""(empty string) "C# was developed around 2000 by Microsoft as part of its .NET initiative"
"C# was developed around 2000 by Microsoft as part of its .NET initiative" ""(empty string) ".NET" "C# was developed around 2000 by Microsoft as part of its"
"C# was developed around 2000 by Microsoft as part of its .NET initiative" "C#" ""(empty string) "was developed around 2000 by Microsoft as part of its .NET initiative"
"C# was developed around 2000 by Microsoft as part of its .NET initiative" "C#" ".NET" "was developed around 2000 by Microsoft as part of its"
"C# was developed around 2000 by Microsoft as part of its .NET initiative" ".NET" ""(empty string) "initiative"
"C# was developed around 2000 by Microsoft as part of its .NET initiative" ""(empty string) "C#" ""(empty string)
"C# was developed around 2000 by Microsoft as part of its .NET initiative" ".NET" "C#" ""(empty string)
"C# was developed around 2000 by Microsoft as part of its .NET initiative" "ABC" "C#" ""(empty string)
"C# was developed around 2000 by Microsoft as part of its .NET initiative" ".NET" "XYZ" ""(empty string)
"C# was developed around 2000 by Microsoft as part of its .NET initiative" "ABC" "XYZ" ""(empty string)

Rules

This is . The answer with the fewest bytes wins.

\$\endgroup\$
17
  • 7
    \$\begingroup\$ If "as" and "" are given as start and end keywords, should the output be "developed around 2000 by Microsoft as part of its .NET initiative" (everything after "was", which contains "as") or "part of its .NET initiative" (everything after the exact word "as")? \$\endgroup\$
    – Bubbler
    Jan 7, 2021 at 4:06
  • 3
    \$\begingroup\$ Also, what should we do if the given keyword appears multiple times in the string? If the string is "abc 1 def 2 abc 3 def", delimiting from "abc" to "def" may result in "1", "3", or "1 def 2 abc 3". \$\endgroup\$
    – Bubbler
    Jan 7, 2021 at 4:08
  • 5
    \$\begingroup\$ Suggested test case: "", ".NE?T", which should return an empty string but will fail if unescaped regular expressions are used. \$\endgroup\$
    – Arnauld
    Jan 7, 2021 at 4:39
  • 5
    \$\begingroup\$ Please make sure the challenge is specified fully in the text, so that readers don't need to study the test cases to figure out what's intended. \$\endgroup\$
    – xnor
    Jan 7, 2021 at 6:30
  • 6
    \$\begingroup\$ @JimmyHu You didn't answer my second comment. Does the "first occurrence" rule apply to both start and end keywords? Should "abc 1 def 2 abc 3 def", "abc", "def" give "1" then? \$\endgroup\$
    – Bubbler
    Jan 7, 2021 at 6:50

14 Answers 14

5
\$\begingroup\$

JavaScript (ES6),  80  75 bytes

This contains some unprintable characters which are escaped below.

(s,a,b)=>s.replace(b||/$/,"").replace(a,"").match(/ *(.*?) *|$/)[1]||""

Try it online!

Commented

(s, a, b) =>          // s = input string, a = start keyword, b = end keyword
  s.replace(          // replace in s:
    b || /$/,         //   look for the end keyword, or the regex /$/ if it's empty
    "\3"              //   and replace it with ETX (end of text)
  )                   //
  .replace(           // replace in the resulting string:
    a,                //   look for the start keyword
    "\2"              //   and replace it with STX (start of text)
  )                   //
  .match(             // attempt to match:
    /\2 *(.*?) *\3|$/ //   "\2"    STX
  )                   //   " *"    followed by optional whitespace
                      //   "(.*?)" followed by a non-greedy string (the payload)
                      //   " *"    followed by optional whitespace
                      //   "\3"    followed by ETX
                      //   "|$"    OR match an empty string to make sure that
                      //           match() doesn't return null
  [1] || ""           // return the payload string, or an empty string if undefined
\$\endgroup\$
2
  • \$\begingroup\$ You need to switch your replace calls in case the keywords overlap, e.g. keywords wa and as should fail on the example string. \$\endgroup\$
    – Neil
    Jan 9, 2021 at 18:46
  • \$\begingroup\$ @Neil Thank you for pointing this out. \$\endgroup\$
    – Arnauld
    Jan 9, 2021 at 20:49
3
\$\begingroup\$

Python 3, 86 77 75 bytes

Saved 9 bytes thanks to movatica!!!
Saved 2 bytes thanks to ovs!!!

lambda s,a,b:s[s.find(a):(b in s)*s.find(b)if b else None][len(a):].strip()

Try it online!

\$\endgroup\$
13
  • 1
    \$\begingroup\$ @movatica No, because the a in s will make it s[len(a)-1:0] so an empty string will be returned. \$\endgroup\$
    – Noodle9
    Jan 8, 2021 at 13:00
  • 1
    \$\begingroup\$ @movatica Added proper testcase for this, your change fails. \$\endgroup\$
    – Noodle9
    Jan 8, 2021 at 13:04
  • 1
    \$\begingroup\$ @movatica Ah, now that's a good one - thanks! :D \$\endgroup\$
    – Noodle9
    Jan 8, 2021 at 14:12
  • 1
    \$\begingroup\$ 74 bytes by using None instead of len(s). \$\endgroup\$
    – ovs
    Jan 8, 2021 at 14:29
  • 1
    \$\begingroup\$ @movatica Magical - thanks! :D \$\endgroup\$
    – Noodle9
    Jan 8, 2021 at 14:45
2
\$\begingroup\$

APL (Dyalog Extended), 24 bytes (SBCS)

Full program that prompts for array of [EndKeyword,StartKeyword,InputString]. Requires 0-based indexing.

⌂deb⊃(⌽⊢↓⍨1⍳⍨⊣,⍷)/⌽¨@0⊢⎕

Try it online!

 prompt for input

 on that…

⌽¨@0 reverse all the elements that occur at offset 0

()/ reduce from the right using the following tacit function:

 indicate with a Boolean list all the places where the left argument begins in the right argument

⊣, prepend the left argument to that

1⍳⍨ find the offset of the first 1

⊢↓⍨ drop that many leading elements from the right argument

reverse (next time around, do this from the end, and after that, revert order)

 disclose the enclosure caused by the reduction from a 1-dimensional array to a 0-dimensional array

⌂debdelete ending (leading and trailing) blanks

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

JavaScript (Node.js), 74 bytes

(s,a,b)=>s.substr(p=(s+a).indexOf(a)+a.length,b?s.indexOf(b)-p:1/0).trim()

Try it online!

Quite straightforward...

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

Ruby, 66 bytes

->w,s,e,r=Regexp{"#{w[/#{r.quote s}\K.+(?=#{r.quote e})/]}".strip}

Try it online!

Another method without the use of regex,

Ruby, 72 bytes

->w,s,e{"#{w[((w+s).index(s)+s.size rescue 0)...w.rindex(e)||0]}".strip}

Try it online!

\$\endgroup\$
4
  • 1
    \$\begingroup\$ f[s ,"foo" ,"" ] should return an empty string. \$\endgroup\$
    – Arnauld
    Jan 7, 2021 at 5:10
  • \$\begingroup\$ @Arnauld - thanks for pointing that out, edited now! \$\endgroup\$
    – vrintle
    Jan 7, 2021 at 6:19
  • 1
    \$\begingroup\$ Seems OP use (left) index of for end words. Is rindex allowed? \$\endgroup\$
    – tsh
    Jan 7, 2021 at 6:26
  • 1
    \$\begingroup\$ Neither solution gives the empty string for keywords of wa and as... \$\endgroup\$
    – Neil
    Jan 9, 2021 at 18:47
1
\$\begingroup\$

Python 3, 100 85 bytes

Regex version, still cannot beat the slicing algorithm.

from re import*
r=escape
f=lambda s,b,e:(search(r(b)+'(.+)'+r(e),s)or'  ')[1].strip()

Try it online!

\$\endgroup\$
2
  • 1
    \$\begingroup\$ The result for keywords of wa and as should be the empty string. \$\endgroup\$
    – Neil
    Jan 9, 2021 at 19:04
  • \$\begingroup\$ you're right, it's still buggy \$\endgroup\$
    – movatica
    Jan 9, 2021 at 20:43
1
\$\begingroup\$

Retina 0.8.2, 60 bytes

(.*)¶(.+)?¶.*?\1 *(.*?) *(?<!(?=\2).*)(?(2)\2.*|$)|(.|¶)+
$3

Try it online! Takes input as start, end, string on separate lines but link is to test suite with header that converts from comma separated string, end, start for convenience. Explanation:

(.*)¶

Match the start keyword.

(.+)?¶

Optionally match a non-empty end keyword.

.*?\1

Find the start keyword as early as possible in the string, plus optional spaces.

 *(.*?) *

Match as short a result as possible (so that the end keyword is found as early as possible in the string) but also trim spaces around it.

(?<!(?=\2).*)

Ensure that the end keyword hasn't already been passed at this point.

(?(2)\2.*|$)

If the end keyword was empty then only match at the end of the string otherwise match the end keyword and the rest of the string.

|(.|¶)+

If it wasn't possible to match anything, delete everything.

$3

Keep the desired result.

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

Wolfram Language (Mathematica), 93 bytes

sStringTrim@StringTake[s,i=1;If[i*=-1;#=="",0,StringPosition[s,#][[1,i]]]-i&/@#]/._@_:>""&

Try it online!

\$\endgroup\$
2
  • \$\begingroup\$ The first solution doesn't give the right answer for keywords of wa and as and the second solution doesn't load for me at all. \$\endgroup\$
    – Neil
    Jan 9, 2021 at 18:56
  • \$\begingroup\$ @Neil Thanks. I hadn't updated the answer for the clarified spec. Somehow the second link had a stray </b> in it, but it would have had the same problem anyways. \$\endgroup\$
    – att
    Jan 9, 2021 at 20:56
1
\$\begingroup\$

Red, 90 bytes

func[t s e][p:""if""<> s[append s" "]if e =""[e:[end]]parse t[thru s copy p to[opt" "e]]p]

Try it online!

\$\endgroup\$
2
  • \$\begingroup\$ The result for keywords of wa and as should be the empty string. \$\endgroup\$
    – Neil
    Jan 9, 2021 at 19:00
  • \$\begingroup\$ @Neil Thank you for pointing me this bug - it should be fixed now. \$\endgroup\$ Jan 10, 2021 at 8:56
1
\$\begingroup\$

C (gcc), 168 152 143 132 112 bytes

An enormous -38 thanks to @ceilingcat

#define r strstr(c
*f(c,s,e)int*c,*s,*e;{return*e&&r,s)>r,e)|!r,s)|!r,e)||*e&&(*r,e)=0)?"":r,s)+strlen(s)+!!*s;}

Try it online!

\$\endgroup\$
13
  • \$\begingroup\$ It's perfectly fine to not build a fully functional program, but it should be at least some callable with defined I/O like a function or a lambda. In your case something like char*f(char*s,char*i,char*e){return ...} would be acceptable \$\endgroup\$
    – movatica
    Jan 10, 2021 at 17:18
  • \$\begingroup\$ Here is a TIO with your code. This is the preferred format here. Note that you're using library functions, thus the #import<string.h> statement counts as well, while the boilerplate for a full program does not! \$\endgroup\$
    – movatica
    Jan 10, 2021 at 17:26
  • \$\begingroup\$ Thank you very much @movatica! So now if I make a working function I need to count #include <string.h> but I can omit to count #include <stdio.h> right? and do I need to count the new line between the header and the function as 1 byte? \$\endgroup\$
    – anotherOne
    Jan 10, 2021 at 17:34
  • \$\begingroup\$ My pleasure and welcome to Codegolf! Yes, you got that right and whitespace counts just as well. \$\endgroup\$
    – movatica
    Jan 10, 2021 at 17:37
  • 1
    \$\begingroup\$ Thank you for everything! I am ready to golf! :) \$\endgroup\$
    – anotherOne
    Jan 10, 2021 at 17:57
0
\$\begingroup\$

JavaScript (ES6) 95 92 Bytes, No Regex!

(i,s,e,t=i.indexOf(s),r=i.lastIndexOf(e))=>t!=-1&r!=-1?(i.substring(t+s.length,r)).trim():''

How to try it:

Open your browser's JavaScript Console and paste the following.

((i,s,e,t=i.indexOf(s),r=i.lastIndexOf(e))=>t!=-1&r!=-1?(i.substring(t+s.length,r)).trim():'')('C# was developed around 2000 by Microsoft as part of its .NET initiative', 'C#', '.NET')
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Note that you can add header and footer code in TIO that are ignored in the byte count so you can display the function result, something like this based on your test code or you can check the other Js answers with functions to see how they do it \$\endgroup\$
    – Kaddath
    Jan 7, 2021 at 11:04
  • \$\begingroup\$ Your use of lastIndexOf seems to be incorrect; I think it should just be indexOf. You should also use slice because substring does weird things if the second string appears before the first. After that your answer works properly for keywords of wa and as. \$\endgroup\$
    – Neil
    Jan 9, 2021 at 18:52
0
\$\begingroup\$

Charcoal, 41 bytes

≔⎇ζ…θ⌕θζθθ≔⎇η⪫Φ⪪θηκηθθ≔⌕AEθ›ι ¹ε¿ε✂θ⌊ε⊕⌈ε

Try it online! Link is to verbose version of code. Take care to include sufficient newlines in the input even if one of the keywords is empty. Explanation:

≔⎇ζ…θ⌕θζθθ

If the end keyword is not empty then truncate the string at its first appearance. (Fortunately CycleChop truncates the string to empty if its input is negative.)

≔⎇η⪫Φ⪪θηκηθθ

If the start keyword is not empty then split the string on the keyword, discard the first element, and join the string again. This results in an empty string if the start keyword does not appear in the string.

≔⌕AEθ›ι ¹ε

Check whether the string has any non-spaces.

¿ε✂θ⌊ε⊕⌈ε

If so then print from the first to the last non-space.

\$\endgroup\$
0
\$\begingroup\$

R, 111 bytes

function(s,a,b,c=?s,`?`=nchar,r=regexpr)trimws(substr(s,`if`((d=r(a,s,f=T))>0,d+?a,c),`if`(?b,r(b,s,f=T)-1,c)))

Try it online!

Straightforward approach: finds bounding words using regexpr (with argument fixed=True to ensure that the text string is not interpreted as a regex), gets the substring between them, and then trims the whitespace from both ends.

Since the functions nchar and regexpr are each used twice, it's shorter to define single-character aliases. In the case of nchar, we can even redefine the unary operator ? as its alias, so that we avoid the need for parentheses. Unfortunately, this trick isn't possible here for regexpr because of the need to feed it the additional argument fixed=True.

\$\endgroup\$
0
\$\begingroup\$

C# 114 bytes

(i,s,e)=>{int p=(i+(s??="")).IndexOf(s)+s.Length,q=$"{e}"==""?i.Length:i.IndexOf(e);return p<q?i[p..q].Trim():"";}
\$\endgroup\$

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.