Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to move the decimal point N places to the left efficiently?

I have a bunch of decimal numbers (as strings) which I receive from an API. I need to 'unscale' them, i.e. divide them by some power of 10. This seems a simple task for integers, but I have decimals with no guaranteed range. So, basically I need a function that works like this:

move_point "12.34" 1; # "1.234"
move_point "12.34" 5; # "0.0001234"

I'd rather not use floats to avoid any rounding errors.

like image 739
planetp Avatar asked Jul 04 '16 20:07

planetp


2 Answers

This is a bit verbose, but should do the trick:

sub move_point {
    my ($n, $places) = @_;

    die 'negative number of places' if $places < 0;
    return $n if $places == 0;

    my ($i, $f) = split /\./, $n;  # split to integer/fractional parts

    $places += length($f);

    $n = sprintf "%0*s", $places+1, $i.$f;  # left pad with enough zeroes
    substr($n, -$places, 0, '.');  # insert the decimal point
    return $n;
}

Demo:

my $n = "12.34";

for my $p (0..5) {
    printf "%d  %s\n", $p, move_point($n, $p);
} 

0  12.34
1  1.234
2  0.1234
3  0.01234
4  0.001234
5  0.0001234
like image 122
Eugene Yarmash Avatar answered Sep 20 '22 08:09

Eugene Yarmash


Unless your data has contains values with significantly more digits than you have shown then a floating-point value has more than enough accuracy for your purpose. Perl can reliably reproduce up to 16-digit values

use strict;
use warnings 'all';
use feature 'say';

say move_point("12.34", 1); # "1.234"
say move_point("12.34", 5); # "0.0001234"
say move_point("1234", 12);
say move_point("123400", -9);

sub move_point {
    my ($v, $n) = @_;

    my $dp = $v =~ /\.([^.]*)\z/ ? length $1 : 0;
    $dp += $n;
    $v /= 10**$n;

    sprintf '%.*f', $dp < 0 ? 0 : $dp, $v;
}

output

1.234
0.0001234
0.000000001234
123400000000000



Update

If the limits of standard floating-point numbers are actually insuffcient for you then the core Math::BigFloat will do what you need

This program shows a number with sixteen digits of accuracy, multiplied by everything from 10E-20 to 10E20

use strict;
use warnings 'all';
use feature 'say';

use Math::BigFloat;

for ( -20 .. 20 ) {
    say move_point('1234567890.1234567890', $_);
}

sub move_point {
    my ($v, $n) = @_;

    $v = Math::BigFloat->new($v);

    # Build 10**$n
    my $mul = Math::BigFloat->new(10)->bpow($n);

    # Count new decimal places
    my $dp = $v =~ /\.([^.]*)\z/ ? length $1 : 0;
    $dp += $n;

    $v->bdiv($mul);
    $v->bfround(-$dp) if $dp >= 0;
    $v->bstr;
}

output

123456789012345678900000000000
12345678901234567890000000000
1234567890123456789000000000
123456789012345678900000000
12345678901234567890000000
1234567890123456789000000
123456789012345678900000
12345678901234567890000
1234567890123456789000
123456789012345678900
12345678901234567890
1234567890123456789
123456789012345678.9
12345678901234567.89
1234567890123456.789
123456789012345.6789
12345678901234.56789
1234567890123.456789
123456789012.3456789
12345678901.23456789
1234567890.123456789
123456789.0123456789
12345678.90123456789
1234567.890123456789
123456.7890123456789
12345.67890123456789
1234.567890123456789
123.4567890123456789
12.34567890123456789
1.234567890123456789
0.1234567890123456789
0.01234567890123456789
0.001234567890123456789
0.0001234567890123456789
0.00001234567890123456789
0.000001234567890123456789
0.0000001234567890123456789
0.00000001234567890123456789
0.000000001234567890123456789
0.0000000001234567890123456789
0.00000000001234567890123456789
like image 32
Borodin Avatar answered Sep 17 '22 08:09

Borodin