15
$\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 : {"Hello ... blabla ... Goodbye"}.

Edit

If there are several sequences Hello ...Goodbye (not nested), It should make a List of them.

$\endgroup$
8
$\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$
  • $\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 '13 at 22:16
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
    $\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 '13 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$
  • $\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 '13 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 '13 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 '13 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 '16 at 15:25
3
$\begingroup$

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, privacy policy and cookie policy

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