One task that I frequently find myself doing in Mathematica is splitting lists into lists of sublists, using specific elements to define the break-points. This is particularly useful with imported files which often have a structure like this:
section 1
a
b
c
section 2
d
e
f
Upon import, this turns into
{"section 1", "a", "b", "c", "section 2", "d", "e", "f"}
I'd like to split this into sublists at the strings that mark sections, like so:
{{"a", "b", "c"}, {"d", "e", "f"}}
Doing this with Mathematica's library functions is pretty awkward. First, you want a predicate that tells you where the delimiting elements are:
delimiterQ[s_String] := StringMatchQ[s, "section "~~__]
delimiterQ[_] := False
In cases like this, rule replacement seems like an elegant solution:
In[1]:= delimitingRule = {before___, _?delimiterQ,
Longest[run : Except[_?delimiterQ] ...],
after___} :> {before, {run}, after};
{"section 1", "a", "b", "c", "section 2", "d", "e", "f"} //. delimitingRule
Out[1]= {{"a", "b", "c"}, {"d", "e", "f"}}
However, the performance of this kind of solution is truly atrocious, as you might expect (I got bored of waiting with a test list of 1000 elements after a couple minutes). The alternative I usually use is:
In[2]:= splitAtDelimiters[l_List, test_] :=
With[{delimIndices = Flatten@Position[l, _?test, {1}],
len = Length@l},
With[{first = First@delimIndices, last = Last@delimIndices},
Take[l, #] & /@ Join[
If[1 < first, {{1, first - 1}}, {}],
# + {1, -1} & /@ Partition[delimIndices, 2, 1],
If[last < len, {{last + 1, len}}, {}]]]];
In[3]:= splitAtDelimiters[
{"section 1", "a", "b", "c", "section 2", "d", "e", "f"},
delimiterQ]
Out[3]= {{"a", "b", "c"}, {"d", "e", "f"}}
This works OK, but I always think there must be a better way to do things whenever I generate a list of indices into a list just to immediately extract them all again. That sort of round trip is a Mathematica "code smell", IMO, and in this particular case there's a lot of extra work done to deal with corner cases involving delimiters at the begining or end of the list.
Any other suggestions? I think that there must be a way to do it with Split
, but after trying a few obvious things I'm not sure what it is.
EDIT to add: My approach using Position
has an annoying bug in cases where you are interested in splitting a list of lists (which is common when processing, say, Excel imports). The corrected version requires use of the Heads -> False
option for Position
:
splitAtDelimiters[l_List, test_] :=
With[{delimIndices =
Flatten@Position[l, _?test, {1}, Heads -> False], len = Length@l},
With[{first = First@delimIndices, last = Last@delimIndices},
Take[l, #] & /@ Join[
If[1 < first, {{1, first - 1}}, {}],
# + {1, -1} & /@ Partition[delimIndices, 2, 1],
If[last < len, {{last + 1, len}}, {}]]]]
Bugs like this make me think my unease with this approach is justified.
EDIT the second: rcollyer suggested I move this from StackOverflow.