34

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]
rm -rf
  • 88,781
  • 21
  • 293
  • 472
M.R.
  • 31,425
  • 8
  • 90
  • 281

4 Answers4

34

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

Kuba
  • 136,707
  • 13
  • 279
  • 740
  • 2
    Oh great, now my "smoking crack" comment is immortalized. :-p – Mr.Wizard Aug 26 '13 at 20:52
  • 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. – Mr.Wizard Aug 26 '13 at 22:14
  • @Mr.Wizard I see, well just a coincidence that I've though it is normal thing to use Span :) – Kuba Aug 26 '13 at 22:16
  • 7
    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). – Verbeia Aug 27 '13 at 00:10
  • Actually, I look at code. For me, the weirder the better, so Mr.W's often matches well for me. This, however, I just noticed. +1 – rcollyer Aug 27 '13 at 00:48
  • +1. Nice discovery. The performance is even more impressive for larger lists. Just for fun I increased the data size to big = Range[1*^6]. Here are the results: {64.703125, 0.031250} on my machine. Just for comparison, the original data gave the following results on my PC: {4.781250, 0.015625} – RunnyKine Aug 27 '13 at 03:17
  • @Runny Yes, the old MapAt (and v9 with the old syntax) is a known slow function with poor computational complexity, so yes, the longer the list the greater the difference will be. – Mr.Wizard Aug 27 '13 at 03:26
  • @Mr.Wizard good to know. Thanks for the link. – RunnyKine Aug 27 '13 at 03:40
  • @Verbeia Good point, I should add this, but yes it seems to have full functionality. I've added two links which shows the usage. I'm also wondering if this is hidden for purpose... – Kuba Aug 27 '13 at 07:02
  • @Kuba most of the time "undocumented" implies "use at your own risk". But, it could just be an oversight. – rcollyer Aug 27 '13 at 13:18
  • @rcollyer I think it is not finished/tested too. It is so cool that I would not forget to announce this as an author :) – Kuba Aug 27 '13 at 13:22
  • @Kuba I still think it could go either way. That said, I don't know, and if I find out, I might not be able to say. However, the ability to write MapAt[#^2 &, Transpose[{Range[5], {a, b, c, d, e}}], {2 ;; 4, 2}] is awesome. Great find. – rcollyer Aug 27 '13 at 13:24
  • Great find! I can't believe it's taken WRI so long to implement this. – Simon Woods Aug 27 '13 at 15:09
  • One more vote on my answer and you pick up a rare Gold badge. :-) – Mr.Wizard Apr 20 '14 at 06:02
  • @Mr.Wizard That would be of course great but since this answer is a result of a lucky coincidence and not my effort/knowledge it may be though for me to fully appreciate it :) But 80% is enough to be excited :P – Kuba Apr 20 '14 at 07:11
  • 1
    @Kuba, just noticed this earlier Q/A using the same hidden v9 update:) – kglr May 24 '14 at 09:06
  • @kguler :) it was natural for me too, till the point that Mr. Wizard pointed it to me :) I guess it's because I don't trust docs 100% anymore and I'm checking it only when something fails :p – Kuba May 24 '14 at 09:12
13

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
  ]
Rojo
  • 42,601
  • 7
  • 96
  • 188
Mr.Wizard
  • 271,378
  • 34
  • 587
  • 1,371
  • Definately smarter +1 – Rojo May 28 '12 at 08:32
  • @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. – Mr.Wizard May 28 '12 at 08:42
4

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

Rojo
  • 42,601
  • 7
  • 96
  • 188
2

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

Heike
  • 35,858
  • 3
  • 108
  • 157