TransWikia.com

How to use each replacement only once in a list of replacements

Mathematica Asked on October 22, 2021

I have a list of replacements and some list they are acting on. However, some RHS of the replacements rules are identical (the LHS is not). I want to use those rules one after another (perhaps cycling after they run out but the amount of replacements and occurrences match in the application I am looking at now).

So for example

replaceOnceOnly[{a, b, a}, {a -> 1, a -> 2, b -> 3}]

would output {1,3,2}. Is there a simple way to achieve this?


I should have included the following:
The replacement should act only on the whole of the list entry. I.e. the RHS is always an explicit element of the list the replacement acts on. It should not act on replaceOnceOnly[{f[a]},{a->1}] should not do anything. (Nor shall such an element even occur in the list to be acted on.) Also I want the replacements to act in a clear order: so from left to right for example.

(The other case is certainly interesting too but not what I am looking for.)

4 Answers

Here a solution which will adjust the behaviour of a set of rules which contains "duplicate" rules:

adjust[strategy_, rules_] :=
  Hold@@@GatherBy[rules, First] //
  Map[With[{vs = #[[All, 2]]}, strategy[RuleDelayed@@{#[[1, 1]], Unevaluated@vs}]]&]

cycle[k_ :> vs_] := Module[{i = 0}, k :> vs[[1+Mod[i++, Length@vs]]]]
oneshot[k_ :> vs_] := Module[{i = 0}, k :> Module[{ii = ++i}, vs[[ii]] /; ii <= Length[vs]]]
padlast[k_ :> vs_] := Module[{i = 0}, k :> vs[[Min[++i, Length[vs]]]]]
normal[k_ :> _[v_, ___]] := k :> v

Multiple Strategies

The various strategies are...

... cycle which cycles through the rules repeatedly:

{a, b, a, b, b, b, a, a} /. adjust[cycle, {a -> 1, a -> 2, b -> 3}]

(* {1, 3, 2, 3, 3, 3, 1, 2} *)

... padlast which reuses the last rule as "padding":

{a, b, a, b, b, b, a, a} /. adjust[padlast, {a -> 1, a -> 2, b -> 3}]

(* {1, 3, 2, 3, 3, 3, 2, 2} *)

... oneshot which just lets the rules run out:

{a, b, a, b, b, b, a, a} /. adjust[oneshot, {a -> 1, a -> 2, b -> 3}]

(* {1, 3, 2, b, b, b, a, a} *)

... and normal which is the regular behaviour where extra "duplicates" are ignored:

{a, b, a, b, b, b, a, a} /. adjust[normal, {a -> 1, a -> 2, b -> 3}]

(* {1, 3, 1, 3, 3, 3, 1, 1} *)

RuleDelayed Replacements

The solution also supports replacements that use RuleDelayed (:>).

Given:

$rules =
  { f[x_] :> x, f[x_] :> 10x, f[x_] :> 100x
  , g[x_] :> -x, g[x_] :> -10x
  , h[x_] :> Echo["Evaluation Leak!"]
  };

$exprs = {f[1], g[2], f[3], g[4], f[5], f[6], g[7]};

Then:

$exprs /. adjust[cycle, $rules]
(* {1,-2,30,-40,500,6,-7} *)

$exprs /. adjust[oneshot, $rules]
(* {1,-2,30,-40,500,f[6],g[7]} *)

$exprs /. adjust[padlast, $rules]
(* {1,-2,30,-40,500,600,-70} *)

$exprs /. adjust[normal, $rules]
(* {1,-2,3,-4,5,6,-7} *)

Limited Support for Condition

Beware that the exhibited implementation only supports Condition (/;) on the left-hand side of a rule:

Range[10] /.
  adjust[cycle, {x_ /; x < 7 :> "small", x_ /; x < 7 :> "little"}]

(* {small,little,small,little,small,little,7,8,9,10} *)

It does not support conditions on the right-hand side, whether "bare" or nested within a scoping construct:

1 /. adjust[cycle, {x_ :> "small" /; x < 7}]

(* incorrect result:   small /; 1 < 7 *)

Answered by WReach on October 22, 2021

This uses FirstPosition and ReplacePart. It works on the OP's example, not sure how extensible it is.

replaceOnceOnly[expr_, rules_] := Module[{val = expr, part},
    Function[
        If[Not[MissingQ[part = FirstPosition[val, #]]],
            val = ReplacePart[val, part -> #2]
        ]
    ]@@@rules;
    val
];
In[14]:= replaceOnceOnly[{a, b, a}, {a -> 1, a -> 2, b -> 3}]

Out[14]= {1, 3, 2}

Answered by Jason B. on October 22, 2021

One way is to use the following:

GeneralUtilities`ListIterator
GeneralUtilities`IteratorExhausted

Then it can be done with Replace or ReplaceAll:

Needs@"GeneralUtilities`"

Module[{hold},
  SetAttributes[hold, HoldAll];
  
  oneTimeRules[rules_] :=
   
   Normal@Merge[rules, ListIterator] /. Rule -> RuleDelayed /. 
     i_GeneralUtilities`Iterator :> 
      With[{r = Read[i]}, hold[r, r =!= IteratorExhausted]] /. 
    hold -> Condition;
  
  ];

Example:

Replace[{a, b, a, a, b, b}, oneTimeRules@{a -> 1, a -> 2, b -> 3}, 1]
(*  {1, 3, 2, a, b, b}  *)

It does not work with patterns:

Replace[{a, b, a, a, b, b}, 
 oneTimeRules@{x_ -> f[x], x_ -> 2, b -> 3}, 1]
(* {f[x], 2, a, a, 3, b}  <-- should be f[a] *)

Addendum

I thought this modification of @Nasser's approach (now deleted), derived from , seemed a better idea. It seems to work with patterns.

ClearAll[useOnce, useRepeated];
SetAttributes[useRepeated, Listable];
useRepeated[(Rule | RuleDelayed)[pat_, repl_], n_ : 1] :=
  Module[{used = 0},
   pat :> repl /; used++ < n
   ];
useOnce[r_] := useRepeated[r];

Replace[{a, b, a, a, b, b}, useOnce@{a -> 1, a -> 2, b -> 3}, 1]
(*  {1, 3, 2, a, b, b}  *)

Replace[{a, b, a, a, b, b}, useOnce@{x_ -> f[x], x_ -> 2 x, b -> 3}, 1]
(*  {f[a], 2 b, a, a, 3, b}  *)

The function useRepeated lets a rule be applied up to n times, by default 1. The function useOnce is shorthand useRepeated with n = 1. The *Iterator family uses similar internal data to keep track of where an Iterator is, so if I were using this, I'd prefer useOnce.

Answered by Michael E2 on October 22, 2021

I think this does the trick. It's a bit ugly and procedural though:

useRulesOnce[item_, rules_] := Module[{result, citem = item, rrules},
  rrules = Reap[Do[
      With[{repl = (citem /. r)}, 
        If[repl =!= citem, citem = repl; 
         (* Print["replacing using " <> ToString[r]] *), Sow[r]]];
      , {r, rules}]] /. Null -> Nothing;
  Return[{citem, Flatten[rrules]}]]

replaceOnceList[list_, rules_] := 
 Module[{newItem, remainingRules = rules},
  Reap[Do[
     {newItem, remainingRules} = useRulesOnce[ item , remainingRules];
     Sow[newItem];
     , {item, list}]][[-1, 1]]
  ]

replaceOnceList[{a, b, a}, {a -> 1, a -> 2, b -> 3}]
(* result: {1,3,2} *)

This works with patterns too, and you can see which rules it chose if you enable the Print comment in useRulesOnce:

replaceOnceList[{Sin[4], Sin[5], a, Sin[3], b, a}, 
 {a -> 1, 
  Sin[x_ /; EvenQ[x]] :> 0,
  Sin[x_ /; OddQ[x]] :> -1,
  b -> 4,
  a -> 5}]

(* replacing using Sin[x_ /; EvenQ[x]] :> 0
   replacing using Sin[x_ /; OddQ[x]] :> -1
   replacing using a -> 1
   replacing using b -> 4
   replacing using a -> 5

   {0, -1, 1, Sin[3], 4, 5} *)

Answered by flinty on October 22, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP