14
$\begingroup$

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.

$\endgroup$

6 Answers 6

5
$\begingroup$

The following works:

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

(* Out[1]= {{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[1]= "x + ybc" *)

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

~~ is otherwise known as StringExpression.

$\endgroup$
4
$\begingroup$

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}]
$\endgroup$
2
  • 3
    $\begingroup$ This depends on months not having any existing definition and is potentially unsafe... $\endgroup$
    – rm -rf
    Apr 14, 2012 at 7:21
  • 2
    $\begingroup$ @R.M Block should fix that. $\endgroup$ Apr 14, 2012 at 8:08
3
$\begingroup$

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[100] 
                         + RandomInteger[{1, 12}] months], RandomReal[100]}, {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
$\endgroup$
3
$\begingroup$

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.

$\endgroup$
2
$\begingroup$

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}}

$\endgroup$
0
2
$\begingroup$

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}}*)
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.