# How can I mix the behaviors of StringReplace and ReplaceAll in a single rule?

I'm trying to take a list of the form { {"65 + 3 months", 75}, {"65 + 4 months", 75.1} }, and transform the string part to a number such as 65.25 or 65.333, respectively.

What I really want to do is something like:

list = { {"65 + 3 months", 75}, {"65 + 4 months", 75.1} };
list /. {y:NumberString~~" + "~~m:NumberString,p_}:>{y+m/12,p}


That form doesn't work, I believe, because I need to use StringReplace to cause the StringExpression to do any matching.

So since I can't make that work, I tried something like:

list /.{ys_String, p_} :>
{StringReplace[ys, y:NumberString~~" + "~~m:NumberString~~" months":>y+m/12], p}


But in that case, I'm getting { { StringExpression[3/12 + 65], 75}, {StringExpression[4/12 + 65], 75.1} }, and have no idea where the StringExpression heads came from in that case or what to do with them without making my expression uglier and even further from just describing the patterns that I'm trying to transform from/to.

Is there a simple way to compose a pattern that does some string matching and some structure matching? If not, what's the best way to write the StringReplace version for the problem above.

The following works:

list /. {ys_String, p_} :> {StringReplace[ys,
y : NumberString ~~ __ ~~ m : NumberString ~~ __ :> y <> "+" <> m <> "/12"], p} //
ToExpression//N

(* Out= {{65.25, 75.}, {65.3333, 75.1}} *)


The reason your construct failed is because the replacement rule, y+m/12 is not a string (you're performing additions and divisions on strings...). The following example makes it clear as to the difference:

StringReplace["abc", "a" -> "x + y"]
(* Out= "x + ybc" *)

StringReplace["abc", "a" -> "x" + "y"]
(* Out= "x" + "y" ~~ "bc" *)


~~ is otherwise known as StringExpression.

This seems to work for this particular case.

list = { {"65 + 3 months", 75}, {"65 + 4 months", 75.1} };

ToExpression[list] /.
{Plus[y_?NumericQ, Times[m_?NumericQ, months]], b_?NumericQ} :> {y + m/12., b}

==> {{65.25, 75}, {65.3333, 75.1}}


Edit:

I agree with the comments that this could be dangerous if months is already declared. If you intend to use this approach it is best to use Blockas @Sjoerd C. de Vries points out.

Block[{months},
ToExpression[
list] /. {Plus[y_?NumericQ, Times[m_?NumericQ, months]],
b_?NumericQ} :> {y + m/12., b}]

• This depends on months not having any existing definition and is potentially unsafe... – rm -rf Apr 14 '12 at 7:21
• @R.M Block should fix that. – Sjoerd C. de Vries Apr 14 '12 at 8:08

The transformation you want isn't adequate enough for pattern matching, so it would be reasonably to add a functional approach, since it is faster in general. A direct approach (it seems to be the simplest and the fastest here) :

{ ToExpression@ #1, #2} & @@@ list /. months -> 1./12

{{65.25, 75}, {65.3333, 75.1}}


Another way, which is recommended for some more sophisticated algebraic simplifications :

Simplify[{ ToExpression@ #1 , #2} & @@@ list, months == 1./12]


The second argument in Simplify is an assumption to make the values of months real by setting 1./12.

Let's compare timings for a longer list :

list = Table[ {ToString[ RandomInteger
+ RandomInteger[{1, 12}] months], RandomReal}, {100000}];


begining from the fastest method :

 {  ToExpression@ #1, #2 } & @@@ list /. months -> 1./12; // AbsoluteTiming // First

1.3050000

 Simplify[{ ToExpression@ #1 , #2} & @@@ list, months == 1./12];//AbsoluteTiming

 1.4840000

 ToExpression[list] /. {Plus[y_?NumericQ, Times[m_?NumericQ, months]],
b_?NumericQ} :> {y + m/12., b}; //   AbsoluteTiming // First (* Andy Ross *)

2.3180000

 list /. {st_String, p_} :>
With[{v =
StringReplace[st,
StartOfString ~~ y : NumberString ~~ " + " ~~ m : NumberString ~~ __ ~~ EndOfString :>
ToExpression@y + ToExpression@m/12]}, {First@v, p} /;
Head[v] === StringExpression]; // AbsoluteTiming // First    (* Rojo *)

3.4470000

  list /. {ys_String, p_} :>
{StringReplace[ys, y : NumberString ~~ __ ~~ m : NumberString ~~ __ :>
y <> "+" <> m <> "/12"], p} // ToExpression // N;//AbsoluteTiming // First (* R.M. *)

3.5330000


A solution could be

list /. {st_String, p_} :> With[{v = StringReplace[st,
StartOfString ~~ y : NumberString ~~ " + " ~~
m : NumberString ~~ __ ~~ EndOfString :>
ToExpression@y + ToExpression@m/12]},

{First@v, p} /; Head[v] === StringExpression]


StringExpression appears because StringReplace is meant to replace substrings with substrings, returning the modified string. If what you build up to replace the substring is not a string, it can't join it with the rest of the string, so it wraps it in StringExpression. This looks weird in cases like this one where we actually want to replace the whole string and not a substring.

This solution replaces every pair with what you want, only if the string had the required structure. If the StringReplace rule doesn't fit, the result (of the StringReplace) is the original string and the ReplaceAll condition isn't fulfilled. When it fits, the result is a StringExpression and only then the ReplaceAll rule is deemed valid.

This may not exactly answer your question, and I won't say anything really new or very different from other answerers, but, after giving it some thought, I would stress extensibility, and a clean separation of concerns. I think that what you really ask for is a simple parser, which can be easily extended when your requirements change, with only minimal modifications (I will interpret your question in this way, anyways). Embedding the replacement inside a rule (which was my first thought, and also in some other answers), will couple string and structural parsing, perhaps a little stronger than I'd think necessary. Anyways, here is my version:

ClearAll[parse];
parse[l : {{_, _} ..} | {_, _}] := parse /@ l;
parse[s_String] :=
With[{res =
StringCases[s, y : NumberString ~~ " + " ~~ m : NumberString :>
N@Total[Map[ToExpression, {y, m}]*{1, 1/12}]]
},
First@res /; res =!= {}];
parse[x_] := x;


Note that it takes into account the structure of your list, but it separates string parsing from structural operations on your list. And this can still be considered a rule-based solution, even though rules are global here (parse). Here is your example:

parse[list]


{{65.25, 75}, {65.3333, 75.1}}

Another ToExpression variant

ToYears[str_] := Block[{months = 1/12}, ToExpression[str]];

list = {{"65 + 3 months", 75}, {"65 + 4 months", 75.1}};
{N[ToYears[First@#]], Last@#} & /@ list

(*{{65.25, 75}, {65.3333, 75.1}}*)