11
$\begingroup$

Suppose I have a list of arbitrary length:

{1, 2, 3, "Open", 3, 2, "Close", 9, 3, 4, "Open", 1, 0, "Close", 3, 5}

and I am trying to extract the sequences delimited by the "Open"/"Close" tags, i.e. the answer I want is:

{{3, 2}, {1, 0}}

What's the right way to do this? The data I am actually working with is a large XML document and I am trying to extract sections of it by identifying certain tags as boundaries. I've fiddled with different pattern matching functions but can't figure out how to operate on a list in this way since I am trying to match patterns on an intermediate level between individual elements and the entire list.

EDIT

To clarify, I don't know in advance how many such sequences the data will contain or how many elements will be present between any particular set of tags.

$\endgroup$
2
  • 1
    $\begingroup$ ugly: Take[list, #] & /@ (Partition[ Flatten[Position[list, "Open" | "Close"]], 2] /. {a_?NumberQ, b_?NumberQ} -> {a + 1, b - 1}) $\endgroup$
    – chuy
    Apr 30, 2013 at 20:37
  • $\begingroup$ Closely related: (941) $\endgroup$
    – Mr.Wizard
    Jul 29, 2017 at 14:13

10 Answers 10

7
$\begingroup$

Try this one

ReplaceList[expr, {__, PatternSequence["Open",v__ /; Count[{v}, _String] == 0, 
            _String], __} -> {v}]
$\endgroup$
5
  • $\begingroup$ If you don't Flatten the data from the xml Case will be a good alternative. $\endgroup$
    – Spawn1701D
    Apr 30, 2013 at 20:51
  • $\begingroup$ Good call on ReplaceList $\endgroup$
    – mfvonh
    Apr 30, 2013 at 20:53
  • $\begingroup$ Maybe change PatternSequence["Open", v__ /; Count[{v}, _?(# == "Close" &)] == 0, "Close"] in case the list contains other strings than "Open"|"Close". $\endgroup$ Apr 30, 2013 at 21:02
  • $\begingroup$ @b.gatessucks yes the correct structure of the pattern will depend on the actual data, if the tags contain also in the data strings the OP should exclude just the xml tags. My opinion is that the XMLObject and XMLElement objects must be exploited instead of a flattened list. $\endgroup$
    – Spawn1701D
    Apr 30, 2013 at 21:09
  • $\begingroup$ I would use the pattern {__, PatternSequence["Open", v : Except["Close"] .., "Close"], __} from the answer below. That should be more efficient that using a test function. $\endgroup$ Jul 29, 2017 at 19:26
8
$\begingroup$

Here's a plain pattern approach, I'm not quite sure how robust it is:

ReplaceList[expr, {___, "Open", x : Except["Close"] ..., "Close", ___} :> {x}]

Also take a look at Longest and Shortest, which may come in handy.

$\endgroup$
5
$\begingroup$

In versions 10.1+, you can use SequenceCases:

 lst={1, 2, 3, "Open", 3, 2, "Close", 9, 3, 4, "Open", 1, 0, "Close", 3, 5};
 SequenceCases[lst, {"Open", x:Except["Close"].., "Close"} :> {x}]

{{3, 2}, {1, 0}}

$\endgroup$
2
  • $\begingroup$ How could one use the Shortest construct here for x? $\endgroup$
    – Syed
    Sep 10, 2023 at 16:13
  • 1
    $\begingroup$ SequenceCases[lst, {"Open", Shortest[x__Integer], "Close"} :> {x}] works, but,for some reason, SequenceCases[lst, {"Open", Shortest[x__], "Close"} :> {x}] does not. $\endgroup$
    – kglr
    Sep 10, 2023 at 17:34
3
$\begingroup$
Split[lst, (#1 =!= "Close" && #2 =!= "Open") &] // 
 Cases[{"Open", x__, "Close"} :> {x}]

{{3, 2}, {1, 0}}

$\endgroup$
1
  • 1
    $\begingroup$ (+1) This is faster! $\endgroup$ Sep 11, 2023 at 2:03
2
$\begingroup$

My humble attempt:

list = {1, 2, 3, "Open", 3, 2, "Close", 9, 3, 4, "Open", 1, 0, 
   "Close", 3, 5};
SplitBy[
 Select[list, 
  (open = # != "Close" && (# == "Open" || open)) &], # == "Open" &] 
    //. "Open" | {} -> Sequence[]
$\endgroup$
2
$\begingroup$

I feel this is way too complicated, but anyway:

l = {1, 2, 3, "Open", 3, 4, 5, 2, "Close", 9, 3, 4, "Open", 0, 
   "Close", "Close", 3, 5};

Reap[l //. {a___, 
     PatternSequence["Open", mid : _?NumericQ .., "Close"], 
     b___} :> {a, Sow[{mid}]; mid, b}][[2, 1]]

{{3, 4, 5, 2}, {0}}

This example might have some limited instructional value showing one convoluted possible use of Sow and Reap.

$\endgroup$
1
$\begingroup$

What about this one?

ReplaceList[list, {___, "Open", x__ /; FreeQ[{x}, "Close"], "Close", ___} -> {x}]

(although amr's one seems better)

$\endgroup$
1
$\begingroup$
positions = (data // {PositionIndex[#]["Open"],PositionIndex[#]["Close"]} & 
// Transpose )

{{4, 7}, {11, 14}}

data[[Span[# + {1, -1}]]] & /@ positions

{{3, 2}, {1, 0}}

$\endgroup$
1
$\begingroup$

Splitting twice:

list = {1, 2, 3, "Open", 3, 2, "Close", 9, 3, 4, "Open", 1, 0, "Close", 3, 5};

Join @@ Cases[{{_?NumberQ, ___}}] @
  Split[SplitBy[list, NumberQ], #1 != {"Open"} && #2 != {"Close"} &]

{{3, 2}, {1, 0}}

$\endgroup$
1
$\begingroup$

Using Subsequences and Cases:

Subsequences[lst] // Cases[{"Open", x__?NumberQ, "Close"} :> {x}]

(*{{3, 2}, {1, 0}}*)
$\endgroup$
1
  • 1
    $\begingroup$ In general, this is feasible as long as the size of the list remains in check. $\endgroup$
    – Syed
    Sep 11, 2023 at 2:57

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.