turnstep / Math-GMP

Math::GMP Perl module for high speed arbitrary size integer math
Other
3 stars 2 forks source link

float-like operations for Math::GMP > 1.8e308 #11

Open hvds opened 1 year ago

hvds commented 1 year ago

One of my long-running projects has recently reached new heights, dealing with integers greater than 1.8e308. That's fine in the core code where I deal with them as integers, but for some peripheral cases I need to do some float handling, and that code suddenly came crashing down.

I've now worked around that by injecting three extra Math::GMP methods, as below. I'm not sure whether some or all of these are appropriate to include in Math::GMP itself; if you think they would be, I'll put some effort into coming up with implementations that are more robust and efficient, and some tests. (And maybe a better name for bfdiv).

I've also implemented a hack to let me get a sprintf("%g", $gmp) that works for large values with string trickery, but I haven't come up with a way to do it more robustly. (I haven't yet tested it, but docs imply that gmp_sprintf would not support a "%Zg" format.)

Let me know what you think.

my $LONG = 300; # approximate

package Math::GMP {
    sub log10 {
        my($self) = @_;
        my $s = "$self";
        my $len = length($s);
        return log($s) / log(10) if $len < $LONG;
        $s =~ s/^(.)/$1./;
        return log($s) / log(10) + $len - 1;
    }
    sub log {
        my($self) = @_;
        return log10($self) * log(10);
    }
    sub bfdiv {
        my($self, $other) = @_;
        my $sself = "$self";
        # corrected from:
        # return $self / "$other" if length($sself) < $LONG;
        return $sself / "$other" if length($sself) < $LONG;
        my $lother = ref($other) ? $other->log : CORE::log($other);
        return exp($self->log - $lother);
    }
}

sub long_sprintf {
    # $format is expected to have a single %g or similar format specifier
    my($format, $large) = @_;
    my $s = sprintf $format, "$large";
    if ($s =~ /inf/i) {
        my $log = $large->log10;
        my $exp = int($log);
        my $rem = $log - $exp;
        $s = sprintf $format, 10 ** $rem;
        $s =~ s{e\d+|$}{e$exp};
    }
    return $s;
}

=head1 SYNOPSIS

  my $large = Math::GMP->new(10) ** 1000;
  my $smaller = $large * 2 / 3;
  say $large->log10;  # 1000
  say $large->log;    # 2302.58509299405
  say $large->bfdiv($smaller);  # 1.5-ish
  say long_sprintf("%.2g", $large);  # 1.00e1000

=head1 METHODS

=over 4

=head2 log10( )

Returns the base-10 logarithm of the invocant as a Perl float.

=head2 log( )

Returns the natural logarithm of the invocant as a Perl float.

=head2 bdfiv($n)

Returns the invocant divided by C<$n> as a Perl float. C<$n> may be a
C<Math::GMP> object or a standard Perl scalar value. Note that this
will use logarithms/exponentiation for large values, which will limit
its accuracy - use bigfloats instead if you need greater precision.

=back

=head1 FUNCTIONS

=over 4

=head2 long_sprintf($format, $value)

Given a sprintf-style format C<$format> with a single format specification
requesting a value in exponential notation, and a C<Math::GMP> object
C<$value>, returns a string displaying C<$value> in the requested manner.

=back

(edited to s/self/sself/ in bfdiv)

shlomif commented 1 year ago

hi. sorry for the late reply. the code seems out of scope of the integral operations only Math-GMP.

hvds commented 1 year ago

I think that's fair in respect of bfdiv(); I see log() and log10() as ways of getting information about a number when you can't do that with a simple conversion. We do support conversion to scalar or string, but when that gives you "inf" there is little more you can do with it.

An alternative might be a brief note in the docs that points to this ticket for some handy utility functions for dealing with very large numbers, which (with appropriate caveats) would let people find them without you needing to take responsibility for maintaining them.