19
$\begingroup$

The pattern Shortest["A"~~__~~"B"] is oriented : It assumes the text is read from left to right and it takes the text between the first "A" and the next "B". Any "A" after the first "A" is considered as normal text.

I want to select a text between the last "A" and the first "B".

This problem can be solved with Except["A"], but I don't find a clean solution when "A" is a string with more than one character.

Example :

StringCases[
 "blabla ...Hello Hello ... blabla ... Goodbye Goodbye ..",  
 Shortest["Hello" ~~ ___ ~~ "Goodbye"]
]

gives: {"Hello Hello ... blabla ... Goodbye"}.

I would like to get {"Hello ... blabla ... Goodbye"}.

Edit

If there are several sequences "Hello...Goodbye" (not nested), I wish to get a list of them.

$\endgroup$

4 Answers 4

9
$\begingroup$
 shortestStringCases[str_String, from_String, to_String] := 
   StringCases[ str, (from ~~ mid___ ~~ to) /; StringFreeQ[mid, {from, to}]]
 shortestStringCases["blah X blah X first Y blah X blah X second Y", "X", "Y"]
 (* {"X first Y", "X second Y"} *)
$\endgroup$
5
  • $\begingroup$ What I like in the solution shortestStringCases[] is the clarity of the code. One see immediately what it does and that it is balanced. May be not efficient but good as specification. $\endgroup$
    – andre314
    Jan 22, 2013 at 22:16
  • $\begingroup$ This requires that from and to are literal strings right? And not StringExpressions. $\endgroup$
    – Kvothe
    Mar 15, 2021 at 16:15
  • $\begingroup$ @Kvothe, you are right. $\endgroup$
    – kglr
    Mar 15, 2021 at 21:04
  • $\begingroup$ This solution gives wrong results for the cases like shortestStringCases["gffghtomatomato12345iconiconictomatomatoiconiconic","tomato","iconic"]. But +1 for the clarity of the code. $\endgroup$ Aug 29, 2022 at 19:47
  • $\begingroup$ More general version of shortestStringCases, which works correcly with multicharacter boundaries: shortestStringCases[str_String,from_String,to_String]:=StringCases[str,match:(from~~mid___~~to)/;StringFreeQ[StringTake[match,{2,-2}],{from,to}]]. $\endgroup$ Aug 30, 2022 at 3:13
10
$\begingroup$

This seems to work:

StringCases["blabla ...Hello Hello ... blabla ... Goobye Goobye ..", 
 Longest[___ ~~ a : "Hello"] ~~ b : Shortest[___ ~~ "Goobye"] :> a ~~ b]

Update

If there are multiple substrings to extract you can use recursion:

extractbetween[str_, x_, y_] := Module[{f},
  f[s_] := StringCases[s, 
   Longest[a___ ~~ x] ~~ b : Shortest[___ ~~ y] :> {f[a], x ~~ b}];
  Flatten@f@str]

extractbetween["blah X first Y blah X second Y X third Y", "X", "Y"]
(* {"X first Y", "X second Y", "X third Y"} *)
$\endgroup$
1
  • 1
    $\begingroup$ Interesting, but I have forgotten to mention that there are several sequences Hello Hello... Goodbye in the same text. That's a problem with your suggestion. $\endgroup$
    – andre314
    Jan 21, 2013 at 23:16
4
$\begingroup$

A possible solution is just to replace your boundary words with single characters. I think what you are venturing into is something akin to look-behind, which I don't think is supported. Anyways here's how I would do it:

boundary = {"Hello", "Goobye"};
limits = {"\[FormalCapitalX]", "\[FormalCapitalY]"};

shift[str_, from_, to_] := StringReplace[str, Rule @@@ Transpose[{from, to}]]

Just to have more then one match, I changed the test string

test = "blabla ...Hello Hello ... blabla ... Goobye
            Goobye .... Hello ... blabla2... Goobye";

shift[#, limits, boundary] & /@ 
  StringCases[shift[test, boundary, limits], 
  Shortest[limits[[1]] ~~ (Except[limits[[1]]] ...) ~~ limits[[2]]]]
(* {"Hello ... blabla ... Goobye"  , "Hello ... blabla2... Goobye"}  *)

Depending on your input, the substitution characters might need to be more carefully selected.

$\endgroup$
4
  • $\begingroup$ Until yet I use only ASCII character (in this problem). I must see how string-patterns interacts with special characters. Further information in half a day. $\endgroup$
    – andre314
    Jan 22, 2013 at 8:07
  • $\begingroup$ I think the trick is that Except doesn't accept a String apart from when the String represents a special character. Strange. May be there's a more general way to force Except to take a String ? $\endgroup$
    – andre314
    Jan 22, 2013 at 19:07
  • 1
    $\begingroup$ @andre I would expect it's a case of how the regular expression is implimented. To get some feeling for it here's a video of converting regular expressions into finite state automaton. The thing is that deteting when you reach a single token is different from keeping a seperate memory of which tokens you have been through and detecting whether they make up a specific series. $\endgroup$
    – jVincent
    Jan 22, 2013 at 23:00
  • $\begingroup$ (3 years later) The right solution with a regular expression has just been added by Alexey Popkov. See below his answer to my question. $\endgroup$
    – andre314
    Apr 4, 2016 at 15:25
4
$\begingroup$

UPDATE: Universal balanced Shortest

In this answer I managed to find a really universal solution through regular expressions:

Clear[ShortestStringBetween]
Options[ShortestStringBetween] = {"IncludeBoundaries" -> False, 
   "BoundaryOverlaps" -> False};
ShortestStringBetween[str_String, start_String, end_String, OptionsPattern[]] :=
  Module[{bInclude = OptionValue["IncludeBoundaries"],
    bOvelap = OptionValue["BoundaryOverlaps"]},
   Which[
    bInclude && Not[bOvelap],
    StringCases[str, RegularExpression[
      StringTemplate["`START`(?:(?!`END`).(?<!`START`))*`END`"][
       <|"START" -> start, "END" -> end|>]]],
    Not[bInclude] && Not[bOvelap],
    StringCases[str, RegularExpression[
       StringTemplate["`START`((?:(?!`END`).(?<!`START`))*)`END`"][
        <|"START" -> start, "END" -> end|>]] -> "$1"],
    Not[bInclude] && bOvelap,
    StringCases[str, RegularExpression[
      StringTemplate["(?<=`START`)(?:(?!`END`).(?<!`START`))*(?=`END`)"][
       <|"START" -> start, "END" -> end|>]]],
    bInclude && bOvelap,
    StringCases[str, match : RegularExpression[
        StringTemplate["(?<=`START`)(?:(?!`END`).(?<!`START`))*(?=`END`)"][
         <|"START" -> start, "END" -> end|>]] :> StringJoin[start, match, end]]
    ]];

Note that the start and end parameters are directly inserted into RegularExpression and therefore must be regular expressions in the Mathematica format. And since PCRE (on which RegularExpression is based) doesn't support infinite repetition within a lookbehind, the start parameter must be a fixed-length regexp or contain alternations of different but pre-determined lengths (for example, "cat|raccoon"). The end parameter has no such restriction. But I haven't tested how this implementation behaves with non-fixed length parameters.

It works correctly in the all test cases:

front = "Hello";
back = "Goodbye";
str = "blabla ...Hello Hello ... blabla ... Goodbye Goodbye ..";
ShortestStringBetween[str, front, back, "IncludeBoundaries" -> True]
{"Hello ... blabla ... Goodbye"}
front = "tomato";
back = "iconic";
str = "gffghtomatomato12345iconiconictomatomatoiconiconic";
ShortestStringBetween[str, front, back]
{"12345", ""}
front = "NotEnd";
back = "End";
str = "NotEndNotEnd1234NotEnd";
ShortestStringBetween[str, front, back]
ShortestStringBetween[str, front, back, "BoundaryOverlaps" -> True]
{"Not"}
{"Not", "1234Not"}

Original answer

One can use here regular expression with Negative Lookahead (?!regex) Before the Match in the same way as shown in this answer:

text="blabla ...Hello .. blabla .. Hello ... blabla ... Goodbye .. blabla .. Goodbye ... \
blabla ...Hello Hello ... blabla ... blabla ... Goodbye .. blabla .. Goodbye ... ";

StringCases[text, "Hello" ~~ RegularExpression["(?:(?!Hello).)*?"] ~~ "Goodbye"]
{"Hello ... blabla ... Goodbye", "Hello ... blabla ... blabla ... Goodbye"}

Or using pure regexes:

StringCases[text, RegularExpression["Hello(?:(?!Hello).)*?Goodbye"]]
{"Hello ... blabla ... Goodbye", "Hello ... blabla ... blabla ... Goodbye"}

A detailed description of this method can be found here.

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