34
$\begingroup$

Span (;;) is very useful, but doesn't work with a lot of functions. Given the following input

list = {{"a", "b", "c"}, {"d", "e", "f", 
   "g"}, {"h", {{"i", "j"}, {"k", "l"}, {"m", "n"}, {"o", "pp"}}}}

We would like

MapAt[Framed, list, 1 ;; 2]
MapAt[Framed, list, {{1, 1}, {2, 2 ;; 3}, {3, 2, 1 ;; 3, 1}}]

to work as expected

enter image description here

Here is my first go at it:

SpanToRange[Span[x_:1,y_:1,z_:1]] := Module[{zNew = z},
    If[x>y && z==1, zNew = -1];
        Range[x, y, zNew]
    ] /; And[VectorQ[{z,y,z}, IntegerQ],
    And @@ Thread[{z,y,z} != 0]]

helper = Function[list,
    Module[{li=list},
        If[FreeQ[li, Span], li,
        li = Replace[li,s_ /; Head[s] =!= Span :> {s}, {1}];
        li = li /. s:_Span :> SpanToRange[s];
        Sequence @@ Flatten[
            Outer[List, Sequence @@ li],
            Depth[Outer[List, Sequence @@ li]]-3]]
    ]
];

protected = Unprotect[Span, MapAt];
Span /: MapAt[func_, list_, s:Span[x_:1,y_:1,z_:1]]:= MapAt[func, 
 list, Thread[{SpanToRange[s]}]];
MapAt[func_, list_, partspec_] /; !FreeQ[partspec, Span] := Module[{f,p = partspec},
    MapAt[func, list, Join[helper /@ p]]
];
Protect[Evaluate[protected]];

But this is far from finished, and the extended down values should support all valid uses of Span such as

MapAt[Framed, list, 3 ;;]
MapAt[Framed, list, ;; ;; 2]
MapAt[Framed, list, ;; 10 ;; 2]
$\endgroup$
1
  • $\begingroup$ I meant to ask this very same question a week ago, but it fell by the way side, +1. $\endgroup$
    – rcollyer
    May 29, 2012 at 1:26

4 Answers 4

34
$\begingroup$

There is a hidden update in V9: MapAt works with Span.

I've checked it does not work on V8 and V7.

enter image description here

I just started to do this once in the past and it worked. I was newbie in Mathematica when there was V8 or V7 so I have not realised it is new till Mr. Wizard poited out in comments that I'm smoking crack :).

I do not remember other case but it is the second, which I can recall, where there is no mark about this in documentation. I do not mean examples, I mean there is no "Last modyfied in 9" for MapAt only "New in 1.".

Couple of examples where I've used it:


I strongly recommend this, it is so handy, and, as Mr. Wizard noticed, fast!

big = Range[1*^5];
First@Timing@MapAt[#^2 &, big, List /@ Range[30000, 40000]] 
First@Timing@MapAt[#^2 &, big, 30000 ;; 40000]
10.202465
0.015600

Extended comparision inspired by RunnyKine:

test = {};
Do[ big = Range[10^i];
    AppendTo[test,
             {i,
              Mean@Last@Last@Reap@Do[
                  Sow@First@Timing@MapAt[#^2 &, big, List /@ Range[3000, 4000]], {10}],
              Mean@Last@Last@Reap@Do[
                  Sow@First@Timing@MapAt[#^2 &, big, 3000 ;; 4000], {10}]
             }]
  , {i, 5, 6.4, .2}]

ListLogPlot[Transpose[test][[2 ;;]], Joined -> True, DataRange -> {5, 6.4}]

enter image description here

$\endgroup$
17
  • 2
    $\begingroup$ Oh great, now my "smoking crack" comment is immortalized. :-p $\endgroup$
    – Mr.Wizard
    Aug 26, 2013 at 20:52
  • $\begingroup$ By the way, I do read other people's code, especially concise code which you often write. Since I expected news such as Span working in MapAt to be heralded I probably just assumed you had coded that without actually testing it, and I was too lazy to ask. $\endgroup$
    – Mr.Wizard
    Aug 26, 2013 at 22:14
  • $\begingroup$ @Mr.Wizard I see, well just a coincidence that I've though it is normal thing to use Span :) $\endgroup$
    – Kuba
    Aug 26, 2013 at 22:16
  • 7
    $\begingroup$ It also works with All, e.g. fakedailydata = Transpose@{DatePlus[{2001, 1}, #] & /@ Range[0, 999], Accumulate[RandomVariate[NormalDistribution[0, 1], {1000}]]}; MapAt[#^2 &, fakedailydata, {All, 2}]. Really handy for time series (date-value pairs). $\endgroup$
    – Verbeia
    Aug 27, 2013 at 0:10
  • 1
    $\begingroup$ @Kuba, just noticed this earlier Q/A using the same hidden v9 update:) $\endgroup$
    – kglr
    May 24, 2014 at 9:06
13
$\begingroup$

I propose using Part instead:

list = {{"a", "b", "c"}, {"d", "e", "f", "g"},
         {"h", {{"i", "j"}, {"k", "l"}, {"m", "n"}, {"o", "pp"}}}};

mapAtSpan[func_, list_, x : Except[_List]] := mapAtSpan[func, list, {{x}}]
mapAtSpan[func_, list_, spec_] :=
 Module[{A = list, f},
   f[x_List] := f /@ x;
   f[x_] := func[x];
   (Part[A, ##] = f@Part[A, ##]) & @@@ Flatten /@ List /@ spec;
   A
 ]

mapAtSpan[Framed, list, {{1, 1}, {2, 2 ;; 3}, {3, 2, 1 ;; 3, 1}}]

Mathematica graphics

Using the same idea

SpannishMapAt[fun_, expr_, {p : Except[_List]} | p : Except[_List]] :=
   SpannishMapAt[fun, expr, {{p}}];
SpannishMapAt[fun_, expr_, p : {{__} ..}] := Block[{A = expr},
  Do[
   A[[Sequence @@ i]] = 
    Map[fun, A[[Sequence @@ i]], {Count[i, _Span | All]}], {i, p}];
  A
  ]
$\endgroup$
2
  • $\begingroup$ Definately smarter +1 $\endgroup$
    – Rojo
    May 28, 2012 at 8:32
  • $\begingroup$ @Rojo actually no, not in its present form. I just realize that my cheap hack making f pseudo-listable breaks this for mapAtSpan[Framed, list, 1 ;; 2] because the parts are lists. I'm not feeling inspired and I'm doing something else right now; maybe you can fix it, or take my idea and do it properly. $\endgroup$
    – Mr.Wizard
    May 28, 2012 at 8:42
4
$\begingroup$

Perhaps

myMapAt[f_, exp_, pos : {__List}] := 
 MapAt[f, exp, 
  Replace[pos, All :> ;;, {2}] //. {bef___, Span[s__], aft___} :> 
    Sequence @@ 
     Thread@{bef, 
       Range @@ ({s} /. 
          With[{l = Length[exp[[bef]]]}, {All :> l, 
            i_?Negative :> l + i + 1}]), aft}]

myMapAt[f_, exp_, pos_List] := myMapAt[f, exp, {pos}];
myMapAt[f_, exp_, pos_] := myMapAt[f, exp, {{pos}}];

This is a question about joining Span and MapAt. This approach reinvents Span and uses MapAt. See MrWizard's solution for a version that reinvents MapAt and uses Span

$\endgroup$
2
  • $\begingroup$ Very close, but what about these cases like: myMapAt[Framed, {1, 2, 3, 4, 5}, 3 ;; -2 ;;] $\endgroup$
    – M.R.
    May 28, 2012 at 7:20
  • $\begingroup$ Forgot about them :). Try $\endgroup$
    – Rojo
    May 28, 2012 at 7:27
2
$\begingroup$

Not very pretty, but you could try something like this

mapAt[f_, exp_, index_Integer] := MapAt[f, exp, index]
mapAt[f_, exp_, List[index__Integer]] := MapAt[f, exp, index]
mapAt[f_, exp_, a_Span] := MapAt[f, exp, Thread[{Range[Length[exp]][[a]]}]]

mapAt[f_, exp_, b : {(_Integer | _Span) ..}] := Module[{rlist, pos},
  pos = Flatten@Position[b, _Span];
  rlist = Fold[
    Function[{prev, p},
     ArrayFlatten[ReplacePart[#, p -> Thread[{Range[Length[
               If[p > 1, 
                Extract[exp, #[[;; p - 1]]], 
                exp]]][[b[[p]]]]}]] & /@ prev]], {b}, pos];
  MapAt[f, exp, rlist]]

mapAt[f_, exp_, b : {{(_Integer | _Span) ..} ..}] :=
 Module[{rlist},
  rlist = Flatten[
    Function[bsub,
      Module[{pos},
       pos = Flatten[Position[bsub, _Span]];
       Fold[Function[{prev, p},
         ArrayFlatten[ReplacePart[#, p -> Thread[{Range[Length[
                   If[p > 1, 
                    Extract[exp, #[[;; p - 1]]], 
                    exp]]][[bsub[[p]]]]}]] & /@ prev]],
        {bsub}, pos]]] /@ b, 1];
  MapAt[f, exp, rlist]]

For the example in the original question mapAt returns

list = {{"a", "b", "c"}, {"d", "e", "f", 
   "g"}, {"h", {{"i", "j"}, {"k", "l"}, {"m", "n"}, {"o", "pp"}}}}

mapAt[Framed, list, ;; 2]
mapAt[Framed, list, {{1, 1}, {2, 2 ;; 3}, {3, 2, 1 ;; 3, 1}}]

Mathematica graphics

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