Incognito has asked for the wisdom of the Perl Monks concerning the following question:

Why Do We Want To Do This?

  • I've written some code in Perl that takes a number and basically converts it to a formatted number, with a specified precision after the decimal point (similar to sprintf()).
  • This code was converted to Javascript (try not to laugh too hard lol!)
  • What we have so far is fine, but I'd like to see if there's anyone out there that knows how to make the regular expression even more cryptic or reduce the number of lines of code we have!

    The Perl Regular Expressions

    # Pad the number with a bunch of zeros $strValue =~ s/^(\d*)\.?(\d*)$/${1}.${2}000000000/; # Remove all but the first '$strPrecision' digits that immediately # trail the decimal point. $strDefaultPrecision = 2; $strPrecision = ($strPrecision >= 0) ? $strPrecision : $strDefaultPrec +ision; if ($strPrecision == 0) { $strValue =~ s/^(\d*)\.?\d*$/$1/; } else { $strValue =~ s/^(\d*\.\d{$strPrecision})\d*$/$1/; }

    The JavaScript Code

    function ConvertNumberToCurrency (strValue, strPrecision) { /* Takes a string that contains a number with optional decimal point an +d returns the number with 2 decimal places. An optional precision parameter is provided. Assumptions: - strValue cannot contain any non-numeric characters (0-9 and .), or + this will not work. - strPrecision must be numeric. - this function works to only 10 digits of accuracy. Usage: ConvertNumberToCurrency (""); ConvertNumberToCurrency (".5"); ConvertNumberToCurrency ("1"); ConvertNumberToCurrency ("3."); ConvertNumberToCurrency ("100", "4"); ConvertNumberToCurrency ("12.4", 1); ConvertNumberToCurrency ("12.56", 0); ConvertNumberToCurrency ("12.46435"); */ var strDefaultPrecision = 2; var regexTruncate; strValue += ""; // Convert any digits to string if (! strValue) { strValue = "0"; } // Convert empty string to 0 // Set any non-numeric values to 0 if (! strValue.match (/^\d+\.?$|\d+\.?\d*$|\d*\.?\d+$/)) { strValue += "0"; } // Add a leading 0 to any numbers starting with a decimal point if (strValue.match (/^\./)) { strValue = "0" + strValue; } // Set the default precision if no precision is provided. strPrecision = (strPrecision >= 0) ? strPrecision : strDefaultPrecis +ion; // Pad the number with a bunch of zeros strReplaceValue = "$1.$2" + "0000000000"; strValue = strValue.replace ( /^(\d*)\.?(\d*)$/, strReplaceValue); // Remove all but the first 'strPrecision' digits that immediately // trail the decimal point.. if (strPrecision == 0) { regexTruncate = new RegExp ("^(\\d*)\\.?\\d*$"); } else { regexTruncate = new RegExp ("^(\\d*\\.\\d{"+strPrecision+"})\\d*$" +); } strValue = strValue.replace (regexTruncate, "$1"); return (strValue); }

    The Question

    Does anyone see a way to make the Perl regular expression(s) or algorithm:
  • more efficient
  • in fewer lines
  • in one regex
    which we will then port over to the JavaScript function shown above? If you've got an idea, hit me with it!

  • Replies are listed 'Best First'.
    Re: Using Regular Expressions to Simulate sprintf() function - One Liner Challenge
    by Albannach (Monsignor) on Nov 06, 2001 at 09:15 UTC
      First a couple of points that may or may not be obvious:
      • you aren't simulating sprintf, just one very tiny part
      • a very useful part of sprintf that you are not simulating is the rounding algorithm (and ++ to Fastolfe for noticing this)

      I quickly threw together a regex (included below) just for my amusement, but then got somewhat confused by the specification and the various interpretations so far. If nothing else this is an interesting exercise in both how different people will interpret the same request, and how many different tests are needed to check the behaviour of even this simple concept. I've added the samples suggested by Incognito and jeffa, as well as one of my own. I haven't bothered to fix my solution, but as you can see, none duplicated sprintf.

      value|precision sprintf Incognito Albannach jeffa '123.45678'|3: 123.457 123.456 123.456 123 '12.4'|1: 12.4 12.4 12.4 12 ''|0: 0 '3.14'|5: 3.14000 3.14000 3.14000 00003.14 '1'|5: 1.00000 1.00000 1.00000 00001 '.5'|0: 1 '10'|5: 10.00000 10.00000 10.00000 00010 '12.45435'|0: 12 12 12 12 '100'|5: 100.00000 100.00000 100.00000 00100 '3.'|0: 3 3 3 3 '1000'|5: 1000.00000 1000.00000 1000.00000 01000 '12.56'|0: 13 12 12 12
      was produced by

        Yes, good point... We're not really simulating sprintf(), but just the part where we take a number and add a decimal place at one part of a number and format it to our liking...

        I think a lot of people didn't notice that the Perl code was just a snippet of what was in the actual function, and that I didn't provide the entire thing... if we examine the JavaScript version, we will notice that it handles the special cases:

        strValue += ""; // Convert any digits to string if (! strValue) { strValue = "0"; } // Convert empty string to 0 // Set any non-numeric values to 0 if (! strValue.match (/^\d+\.?$|\d+\.?\d*$|\d*\.?\d+$/)) { strValue += "0"; } // Add a leading 0 to any numbers starting with a decimal point if (strValue.match (/^\./)) { strValue = "0" + strValue; }

        So what we really have (without actually provided the entire function :) is:

        value|precision sprintf Incognito Albannach jeffa r +ob_au '123.45678'|3: 123.457 123.456 123.456 123 12 +3.457 '12.4'|1: 12.4 12.4 12.4 12 + 12.4 ''|0: 0 0 + 0 '3.14'|5: 3.14000 3.14000 3.14000 00003.14 3. +14000 '1'|5: 1.00000 1.00000 1.00000 00001 1. +00000 '.5'|0: 0 0 + 1 '10'|5: 10.00000 10.00000 10.00000 00010 10. +00000 '12.45435'|0: 12 12 12 12 + 12 '100'|5: 100.00000 100.00000 100.00000 00100 100. +00000 '3.'|0: 3 3 3 3 + 3 '1000'|5: 1000.00000 1000.00000 1000.00000 01000 1000. +00000 '12.56'|0: 13 12 12 12 + 13
    (jeffa) Re: Using Regular Expressions to Simulate sprintf() function - One Liner Challenge
    by jeffa (Bishop) on Nov 06, 2001 at 07:47 UTC
      Again, why a regex?
      use strict; print pad($_,5), "\n" for (qw(3.14 1 10 100 1000)); sub pad { my ($str,$pad) = @_; my ($add,$right); ($str,$right) = split('\.',$str,2); $right = ($right) ? ".$right" : ''; $add = $pad - length($str); return $str . $right if $add < 1; return ('0' x $add) . $str . $right; }
      Sorry, but after reading this i was curious to find a non-regex way. TIMTOWTDI :)

      UPDATE: Woah! I made a HUGE mistake! Thanks to Albannach for pointing it out. Code has been modified:

      old: return $str if $add < 1; new: return $str . $right if $add < 1;
      That was dumb. As a matter of fact, trying to get my code to right zero pad decimals (3.1400), instead of left padding (003.14) as it currently does - and for the record, that is NOT A BUG! it is a feature! - well, that's just dumb. i am going to stick with sprintf. :)

      and don't chastise me about scientific notation, davorg! ;)

      (for the record - for 'some reason' i was thinking the problem was to merely zero pad numbers - oi vey!)

      jeffa

      just say no to testing your new installation of cygwin on Win32 by trying to simulate a small subset of sprintf in Perl and posting it on PerlMonks

    Re: Using Regular Expressions to Simulate sprintf() function - One Liner Challenge
    by Fastolfe (Vicar) on Nov 06, 2001 at 06:51 UTC
      Why use a regex at all?
      # perl $formatted = sprintf("%.2f", $unformatted); # javascript, assuming it doesn't have a s?printf() function formatted = Math.round(unformatted * 100) / 100;
        This is a regex exercise. There are many ways to do this, but I'm doing a discussion with some coworkers on regular expressions... Remember, we're trying to 'simulate' the sprintf() without actually using it.
          s/(\d+(?:\.\d*)?|\d*\.\d+)/sprintf("%.2f", $1)/eg; # :)

          I really can't think of a way to do this in a pure regex. You've got some logic in there (like the addition of zeros where there aren't enough) that I can't think of a way to express in a regular expression while accomplishing the rest of your requirements, short of using program logic like above. Even if there is a solution, I suspect it will make use of Perl's advanced regular expression features and would thus be unportable to JavaScript.

          I'm interested in seeing if there are any good responses, though.

    Re: Using Regular Expressions to Simulate sprintf() function - One Liner Challenge
    by rob_au (Abbot) on Nov 06, 2001 at 12:27 UTC
      As it so happens, blakem and myself were playing with some (obfuscated?) code to perform just this task (precision formatting) for an array of numbers in this thread here. The best I think that I was able to come up with was this:

      sub a{$_=int$_[0]*10**$_[1];s;(.{$_[1]})$;\.$1;;return$_};

      This subroutine, while not particularly robust, takes two arguments, the number to format and the formatting precision - For example, the following code formats to single decimal precision:

      my @a = ("1.10", "2.22"); print STDOUT &a($_, 1) for @a; sub a{$_=int$_[0]*10**$_[1];s;(.{$_[1]})$;\.$1;;return$_};

       

      Update: After running this code through the test-suite written by Albannach, I would probably modify my subroutine as follows to clean up the subroutine output so that a trailing '.' isn't left at the end of a number if there is no decimals to follow it and to add rounding:

      sub a{$_=int$_[0]*10**$_[1]+.5;s;(.{$_[1]})$;\.$1;;chop if substr($_,- +1)eq'.';return$_};

      And the result ...

      value|precision sprintf Incognito Albannach jeffa r +ob_au '123.45678'|3: 123.457 123.456 123.456 123 12 +3.457 '12.4'|1: 12.4 12.4 12.4 12 + 12.4 ''|0: 0 + 0 '3.14'|5: 3.14000 3.14000 3.14000 00003.14 3. +14000 '1'|5: 1.00000 1.00000 1.00000 00001 1. +00000 '.5'|0: 0 + 1 '10'|5: 10.00000 10.00000 10.00000 00010 10. +00000 '12.45435'|0: 12 12 12 12 + 12 '100'|5: 100.00000 100.00000 100.00000 00100 100. +00000 '3.'|0: 3 3 3 3 + 3 '1000'|5: 1000.00000 1000.00000 1000.00000 01000 1000. +00000 '12.56'|0: 13 12 12 12 + 13

       

      Ooohhh, Rob no beer function well without!