Bivio::Type::Number
# Copyright (c) 1999-2007 bivio Software, Inc. All rights reserved. # $Id$ package Bivio::Type::Number; use strict; use base 'Bivio::Type'; use GMP::Mpf (); # C<Bivio::Type::Number> is the abstract base class for all number types. # It provides arbitrary precision arithmetic for like-based numbers. # also uses Bivio::TypeError dynamically my($_FUDGE) = _mpf('0.00000000000000000001'); my($_HALF) = _mpf('0.5'); my($_POWER) = {}; sub abs { my($proto, $v) = @_; ($v = _format($proto, _mpf($v))) =~ s/^-//; return $v; } sub add { my($proto, $v, $v2, $decimals) = @_; # Adds two numbers and returns the result using the specified decimal precision. # If decimals is undef, then the default precision is used. return _format($proto, GMP::Mpf::overload_addeq(_mpf($v), _mpf($v2), 0), $decimals); } sub can_be_negative { my($proto) = @_; # Returns true if L<get_min|"get_min"> is less than 0. return $proto->compare($proto->get_min, 0) < 0 ? 1 : 0; } sub can_be_positive { my($proto) = @_; # Returns true if L<get_max|"get_max"> is greater than 0. return $proto->compare($proto->get_max, 0) > 0 ? 1 : 0; } sub can_be_zero { my($proto) = @_; # Returns true if range crosses through zero. return $proto->compare($proto->get_max, 0) >= 0 && $proto->compare($proto->get_min, 0) <= 0 ? 1 : 0; } sub compare_defined { my($proto, $left, $right, $decimals) = @_; # See L<Bivio::Type::compare_defined|Bivio::Type/"compare_defined">. return _mpf($left) <=> _mpf($right); } sub div { my($proto, $v, $v2, $decimals) = @_; # Divides numerator by denominator and returns the result using the specified # decimal precision. # # Dies if dividing by 0. Bivio::Die->die('divide by zero: ', $v, '/', $v2) if ! defined($v2) || $v2 =~ /^[0.]+$/; return _format($proto, GMP::Mpf::overload_diveq(_mpf($v), _mpf($v2), 0), $decimals); } sub fraction_as_string { my($proto, $number, $decimals) = @_; # Returns the fractional part of I<number> up to decimals without # the leading decimal with rounding. If I<decimals> is zero, always # returns the empty string. return '' if $decimals == 0; my($res) = $proto->round($number, $decimals); $res =~ s/.*\.//; return $res; } sub from_literal { my($proto, $value) = @_; # Makes sure is a number. Does not except scientific notation. # Allows fractional values like "-7 11/15" or "32/3". Fractional # values are converted to decimal using the precision returned by # get_decimals(). $proto->internal_from_literal_warning unless wantarray; return undef unless defined($value) && $value =~ /\S/; # Delete commas and dollar signs $value =~ s/[,\$\)]//g; # Replace parens with minus signs (remove dup minuses) $value =~ s/\(/-/g; $value =~ s/-+/-/g; my($parsed_value); # check for possible "i n/d" format if ($value =~ /\//) { # parse it and convert to decimal my($sign, $integer, $numerator, $denominator) = $value =~ /^([-+])?(\d+\s)?(\d+)\/(\d+)$/; if (defined($denominator) && $denominator != 0) { $value = $proto->add($integer || 0, $proto->div($numerator, $denominator)); if (defined($sign) && $sign eq '-') { $value = $proto->neg($value); } $parsed_value = $value; } } else { # Get rid of all blanks to be nice to user $value =~ s/\s+//g; $parsed_value = $value if $value =~ /^[-+]?(\d+\.?\d*|\.\d+)$/; } # not a number return (undef, Bivio::TypeError->NUMBER) unless defined($parsed_value); # round to the acceptable number of decimals $parsed_value = $proto->round($parsed_value, $proto->get_decimals); # range check return $parsed_value if $proto->compare($parsed_value, $proto->get_min) >= 0 && $proto->compare($parsed_value, $proto->get_max) <= 0; return (undef, Bivio::TypeError->NUMBER_RANGE); } sub get_decimals { # Abstract method to be defined by subclasses. die("abstract method"); } sub mul { my($proto, $v, $v2, $decimals) = @_; # Multiplies two numbers and returns the result using the specified decimal # precision. # If decimals is undef, then the default precision is used. return _format($proto, GMP::Mpf::overload_muleq(_mpf($v), _mpf($v2), 0), $decimals); } sub neg { my($proto, $number) = @_; # Returns a number with the opposite sign from the specified one. return _format($proto, - _mpf($number)); } sub round { my($proto, $number, $decimals) = @_; $decimals = _decimals($proto, $decimals); return _format($proto, _mpf($number), $decimals); } sub sign { my($proto, $number) = @_; # Returns -1, 0, +1 depending on the sign of number. my($sign) = $number =~ /^([-+])/; return $sign eq '-' ? -1 : 1 if defined($sign); return $proto->compare($number, 0) == 0 ? 0 : 1; } sub sub { my($proto, $v, $v2, $decimals) = @_; # Subtracts v2 from v and returns the result using the specified decimal # precision. # If decimals is undef, then the default precision is used. return _format($proto, GMP::Mpf::overload_subeq(_mpf($v), _mpf($v2), 0), $decimals); } sub sum { my($proto, @values) = @_; return $proto->iterate_reduce(sub { return $proto->add(@_); }, \@values); } sub to_literal { my($proto, $value) = @_; # Converts from internal form to a literal string value. return $proto->SUPER::to_literal($value) unless defined($value); # remove leading '+', replace '.1', '-.1' with '0.1', '-0.1' respectively $value =~ s/^\+//; $value =~ s/^\./0./; $value =~ s/^-\./-0./; # remove leading 0s $value =~ s/^0+(\d)/$1/; # remove trailing 0s after decimal point $value =~ s/^(.*\..+?)(0+)$/$1/ unless $value =~ s/\.0*$//; return $value; } sub trunc { my($proto, $number, $decimals) = @_; $decimals = _decimals($proto, $decimals); my($pow) = 10 ** $decimals; return return _format($proto, GMP::Mpf::trunc($number * $pow) / $pow, $decimals); } sub _decimals { my($proto, $decimals) = @_; $decimals = $proto->get_decimals unless defined($decimals); Bivio::Die->die('invalid decimals: ', $decimals) if $decimals < 0; return $decimals; } sub _format { my($proto, $v, $decimals) = @_; # Formats the amount, rounded to the specified number of decimals. $decimals = $proto->get_decimals unless defined($decimals); # add in a fudge factor for for values such as 0.07 which is represented # internally as 0.0699999..., so floor() works correctly. # Mpf seems to always use the lower value, so not needed for negatives $v += $_FUDGE if $v > 0; # round towards +inf my($pow) = $_POWER->{$decimals} ||= 10 ** $decimals; return GMP::sprintf('%.' . $decimals . 'f', GMP::Mpf::floor($v * $pow + $_HALF) / $pow); } sub _mpf { my($value) = @_; # Returns a GMP::Mpf value for the specified string value. unless (defined($value)) { Bivio::IO::Alert->warn_deprecated( 'numeric amount not defined, defaulting to 0'); $value = 0; } # leading + and commas not accepted by GMP $value =~ s/^\+//; $value =~ s/\,//g; # the empty concatenation is very important because it forces values # passed as floats, such as 0.03 which is represented inexactly # to become the literal '0.03' which can be more closely # represented by Mpf (using 500 bits of precision) return GMP::Mpf::mpf($value . '', 500); } 1;