The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!/usr/bin/perl
# t/02.num.t - check for number object
BEGIN
{
use Test::More qw( no_plan );
use strict;
use warnings;
use utf8;
use lib './lib';
use vars qw( $DEBUG );
use POSIX ();
use open ':std' => ':utf8';
my %old = %ENV;
my @rem = qw( LANG LANGUAGE LC_ADDRESS LC_ALL LC_COLLATE LC_CTYPE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE LC_TIME );
no warnings 'uninitialized';
for( @rem )
{
#$ENV{$_} = undef();
delete( $ENV{$_} );
next if( $_ eq 'LC_NAME' || $_ eq 'LC_TYPE' );
# no strict 'refs';
# POSIX::setlocale( &{"POSIX\::$_"}, undef ) if( substr( $_, 0, 3 ) eq 'LC_' );
}
our $DEBUG = exists( $ENV{AUTHOR_TESTING} ) ? $ENV{AUTHOR_TESTING} : 0;
};
use strict;
# diag( "Environment variables: ", Dumper( \%ENV ) );
BEGIN { use_ok( 'Module::Generic::Number' ) || BAIL_OUT( "Unable to load Module::Generic::Number" ); }
POSIX::setlocale( &POSIX::LC_ALL, 'C.UTF-8' ) if( $DEBUG );
my $curr_locale = POSIX::setlocale( &POSIX::LC_ALL );
diag( "Current locale is '$curr_locale'" ) if( $DEBUG );
# require Data::Dump;
# diag( "Environement variables: ", Data::Dump::dump( \%ENV ) );
# RT #132674
# Stupid me. I should compare the result to the locale variables unless they are explicitely set
# my $prev_locale = POSIX::setlocale( &POSIX::LC_ALL );
# my $new_loc = POSIX::setlocale( &POSIX::LC_ALL, 'de_DE' );
my $lconv = POSIX::localeconv();
# $lconv = $Module::Generic::Number::DEFAULT if( !scalar( keys( %$lconv ) ) || ( scalar( keys( %$lconv ) ) == 1 && CORE::exists( $lconv->{decimal_point} ) ) );
$lconv = $Module::Generic::Number::DEFAULT if( !$curr_locale );
# diag( "localeconv contains: ", Data::Dump::dump( $lconv ), "\n" ) if( $DEBUG );
# $lconv->{mon_thousands_sep} = $lconv->{thousands_sep} = undef();
# my $fail = [qw(
# frac_digits
# int_frac_digits
# n_cs_precedes
# n_sep_by_space
# n_sign_posn
# p_cs_precedes
# p_sep_by_space
# p_sign_posn
# )];
# @$lconv{ @$fail } = ( -1 ) x scalar( @$fail );
# POSIX::setlocale( &POSIX::LC_ALL, $prev_locale );
my( $sep_space, $tho_sep, $dec_sep, $grouping, $n );
if( !scalar( keys( %$lconv ) ) || [split(/\./, $curr_locale)]->[0] eq 'C' )
{
diag( "No locale could be found for language \"", ( $ENV{LANG} // '' ), "\"" );
$tho_sep = ',';
$dec_sep = '.';
$grouping = 3;
$n = Module::Generic::Number->new( 10, precision => 2, thousand => $tho_sep, decimal => $dec_sep, grouping => $grouping, debug => $DEBUG );
}
else
{
$tho_sep = CORE::length( $lconv->{thousands_sep} // '' )
? $lconv->{thousands_sep}
: $lconv->{mon_thousands_sep};
$dec_sep = CORE::length( $lconv->{decimal_point} // '' )
? $lconv->{decimal_point}
: $lconv->{mon_decimal_point};
$grouping = CORE::length( $lconv->{grouping} // '' )
? [unpack( "C*", $lconv->{grouping} )]->[0]
: [unpack( "C*", $lconv->{mon_grouping} )]->[0];
$grouping //= 0;
$n = Module::Generic::Number->new( 10, precision => 2, debug => $DEBUG );
}
$sep_space = int( $lconv->{p_sep_by_space} // 0 ) > 0 ? qr/[[:blank:]\h]+/ : '';
if( !defined( $n ) )
{
diag( "Error: '", Module::Generic::Number->error, "'" );
BAIL_OUT( Module::Generic::Number->error );
}
# diag( "Space between symbol and number is '", $n->space, "'." );
# my $lconv_debug = '';
# foreach my $property (qw(
# decimal_point
# thousands_sep
# grouping
# int_curr_symbol
# currency_symbol
# mon_decimal_point
# mon_thousands_sep
# mon_grouping
# positive_sign
# negative_sign
# int_frac_digits
# frac_digits
# p_cs_precedes
# p_sep_by_space
# n_cs_precedes
# n_sep_by_space
# p_sign_posn
# n_sign_posn
# int_p_cs_precedes
# int_p_sep_by_space
# int_n_cs_precedes
# int_n_sep_by_space
# int_p_sign_posn
# int_n_sign_posn
# ))
# {
# my $dots = ( '.' x ( 20 - length( $property ) ) );
# $lconv_debug .= sprintf( qq(%s ${dots}: "%s" (%s) (%d bytes),\n),
# $property, $lconv->{$property}, defined( $lconv->{$property} ) ? 'defined' : 'undefined', length( $lconv->{$property} ) );
# }
# diag( "Locale formatting properties are:\n$lconv_debug" );
my $new_loc = $n->lang;
## diag( "New locale is $new_loc" );
my $n2 = $n->clone;
is( $n2->locale, $new_loc, "Locale is kept with cloning" );
$n2->symbol( '€' );
no warnings;
my $n_fail = Module::Generic::Number->new( 'USD One' );
# diag( Module::Generic::Number->error );
is( $n_fail, undef, 'Invalid number' );
# Creating object from locale
SKIP:
{
my( @paths ) = File::Which::which( 'locale' );
# diag( sprintf( "%d locale executable found", scalar( @paths ) ) );
my @ok_langs;
foreach my $p ( @paths )
{
( @ok_langs ) = eval
{
qx( $p -a );
};
last if( !$@ );
}
# diag( sprintf( "Found %d languages available on the system.", scalar( @ok_langs ) ) );
if( !scalar( @ok_langs ) || !scalar( grep( /^fr_FR/, @ok_langs ) ) )
{
skip( 'Unsupported language', 4 );
}
my $n_loc = Module::Generic::Number->new( 100, { lang => 'fr_FR', precede => 1, precision => 2, thousand => ' ', decimal => ',', debug => 0 });
isa_ok( $n_loc, 'Module::Generic::Number', 'Object with locale language string' );
is( $n_loc->precision, 2, 'French precision => 2' );
# RT #132667
# [:blank:] does not catch non-breaking space, but horizontal space \h does
like( $n_loc->thousand, qr/[[:blank:]\h]+/, 'French thousand separator => space' );
is( $n_loc->decimal, ',', 'French decimal separator => comma' );
};
isa_ok( $n, 'Module::Generic::Number', 'Number Class Object' );
isa_ok( $n2, 'Module::Generic::Number', 'Cloning object' );
is( "$n", 10, 'Stringification' );
isa_ok( ( $n / 2 ), 'Module::Generic::Number', 'Product blessed in Module::Generic::Number' );
is( 4 - $n, -6, 'Subtracting with swap' );
is( $n * 20, 200, 'Multiplication' );
is( ( $n + 100 ) / 2, 55, 'Division' );
is( $n + 100, 110, 'Addition' );
is( $n - 2, 8, 'Subtracting' );
is( $n % 3, 1, 'Modulus with remainder' );
is( $n % 2, 0, 'Modulus without remainder' );
is( $n *= 3, 30, 'Multiplication assignment' );
is( $n /= 5, 6, 'Division assignment' );
is( $n += 2, 8, 'Addition assignment' );
is( $n -= 4, 4, 'Subtraction assignment' );
is( $n %= 2, 0, 'Modulus assignment' );
is( ( $n + 2 ) ** 3, 8, 'Exponent' );
$n += 2;
is( $n **= 3, 8, 'Exponent assignment' );
is( $n & 11, 8, 'Bitwise AND' );
# isa_ok( $n & 11, 'Module::Generic::Number', 'Blessed after Bitwise AND' );
is( $n | 11, 11, 'Bitwise OR' );
# Bitwise XOR
is( $n ^ 11, 3, 'Bitwise XOR' );
is( $n << 2, 32, 'Bitwise shift left' );
is( $n >> 2, 2, 'Bitwise shift right' );
is( $n <<= 2, 32, 'Bitwise shift left assignment' );
is( $n >>= -2, 128, 'Bitwise shift right assignment' );
is( $n x 2, 128128, 'String multiplication' );
is( $n x= 2, 128128, 'String multiplication assignment' );
is( $n .= 4, 1281284, 'String concatenation with numbers' );
ok( $n < 1281285, 'Lower than' );
ok( 10 < $n, 'Lower than (bis)' );
ok( $n lt 1281285, 'Lower than (lt)' );
ok( $n <= 1281284, 'Lower than or equal' );
ok( $n le 1281284, 'Lower than or equal (le)' );
ok( !( $n <= 1281283 ), 'Not lower than or equal' );
ok( $n > 10, 'Higher than' );
ok( $n gt 10, 'Higher than (gt)' );
ok( $n >= 1281284, 'Higher than or equal' );
ok( $n ge 1281284, 'Higher than or equal (ge)' );
is( 10 <=> $n, -1, 'Numerical comparison lower' );
is( 1281284 <=> $n, 0, 'Numerical comparison equal' );
is( 1281285 <=> $n, 1, 'Numerical comparison higher' );
ok( 1281284 == $n, 'Equal' );
ok( $n != 'Hello', 'Not equal string' );
ok( $n != $n2, 'Not equal number' );
ok( $n eq 1281284, 'Equal as string' );
ok( $n ne $n2, 'Not equal as string' );
my $bool = $n != 'Bonjour';
# isa_ok( $bool, 'Module::Generic::Boolean', 'Returning boolean object' );
is( ++$n, 1281285, 'Incrementing' );
is( $n++, 1281286, 'Incrementing (bis)' );
is( $n--, 1281285, 'Decrementing' );
is( --$n, 1281284, 'Decrementing (bis)' );
isa_ok( $n, 'Module::Generic::Number', 'Class object check' );
is( $n . 2, 12812842, 'Concatenation' );
isa_ok( $n, 'Module::Generic::Number', 'Class object check (bis)' );
isa_ok( $n, 'Module::Generic::Number', 'Number regexp check after concatenation' );
is( $n .= 'X', '1281284X', 'String concatenation with non-number' );
isa_ok( $n, 'Module::Generic::Scalar', 'Regexp check after concatenation and class -> Module::Generic::Scalar' );
is( $n2->decimal, $dec_sep, "Decimal separator -> '" . ( defined( $dec_sep ) ? $dec_sep : 'undef' ) . "'" );
# is( $n2->thousand->scalar, $tho_sep, 'Thousand separator' );
# diag( "\$tho_sep is defined? ", defined( $tho_sep ) ? 'yes' : 'no' );
# diag( "\$n2->thousand is defined? ", defined( $n2->thousand ) ? 'yes' : 'no' );
is( ( $n2->thousand // '' ), ( $tho_sep // '' ), "Thousand separator -> '" . ( defined( $tho_sep ) ? $tho_sep : 'undef' ) . "'" );
is( ( $n2->grouping // '' ), ( $grouping // '' ), "Grouping digit -> '" . ( defined( $grouping ) ? $grouping : 'undef' ) . "'" );
is( $n2->precision, 2, "Precision -> '2'" );
is( $n2->currency, '€', "Currency symbol -> '€'" );
isa_ok( $n2->currency, 'Module::Generic::Scalar', 'Returns property as string object' );
diag( "Number to unformat is '$n'" ) if( $DEBUG );
my $n3 = $n2->unformat( $n );
isa_ok( $n3, 'Module::Generic::Number', 'Unformat result in new object using unformat()' );
diag( "Error unformating $n: ", $n2->error ) if( !defined( $n3 ) && $DEBUG );
is( $n3, 1281284, 'Unformat resulting value' );
is( $n3->precision, 2, 'New object precision' );
$dec_sep = '' if( !defined( $dec_sep ) );
$tho_sep = '' if( !defined( $tho_sep ) );
# diag( "Thousand separator is: '", $n3->thousand, "'" );
# diag( "Number::Format object is: ", Dumper( $n3->{_fmt} ) );
is( $n3->format, "1${tho_sep}281${tho_sep}284${dec_sep}00", "Formatting number using format() -> 1${tho_sep}281${tho_sep}284${dec_sep}00" );
is( $n3->currency, '€', "Currency symbol -> '€'" );
my $n_money = $n3->format_money;
if( $n3->precede )
{
like( "$n_money", qr/€${sep_space}1${tho_sep}281${tho_sep}284${dec_sep}00/, 'Formatting money using format_money()' );
}
else
{
like( "$n_money", qr/1${tho_sep}281${tho_sep}284${dec_sep}00${sep_space}€/, 'Formatting money using format_money()' );
}
isa_ok( $n_money, 'Module::Generic::Scalar', 'Returns string object upon formatting' );
$n3 *= -1;
is( $n3, -1281284, 'Negative number' );
like( $n3->format_negative( '(x)' ), qr/\(1${tho_sep}281${tho_sep}284${dec_sep}00\)/, "Formatting negative number => (1${tho_sep}281${tho_sep}284${dec_sep}00)" );
my $n4 = $n3->abs;
is( $n4, 1281284, 'abs' );
# 1.5707955463278
is( $n4->atan, POSIX::atan( $n4 ), 'atan' );
# 1.57078696118977
is( $n4->atan2(12), CORE::atan2( $n4, 12 ), 'atan2' );
my $n5 = $n4->cbrt;
# 108.612997866582
is( $n5, POSIX::cbrt( $n4 ), 'cbrt' );
# 109
is( $n5->ceil, POSIX::ceil( $n5 ), 'ceil' );
# 108
is( $n5->floor, POSIX::floor( $n5 ), 'floor' );
# -0.413777602170324
is( $n4->cos, CORE::cos( $n4 ), 'cos' );
# 20.0855369231877
is( $n4->clone( 3 )->exp, POSIX::exp( 3 ), 'exp' );
# 108
is( $n5->int, CORE::int( $n5 ), 'int' );
ok( !$n5->is_negative, 'Not negative' );
ok( $n3->is_negative, 'Is Negative' );
ok( $n5->is_positive, 'Is positive' );
ok( !$n3->is_positive, 'Is not positive' );
# 14.0633732581021
is( $n4->log, CORE::log( $n4 ), 'log' );
# 20.2891588576344
is( $n4->log2, POSIX::log2( $n4 ), 'log2' );
# 6.10764540293951
is( $n4->log10, POSIX::log10( $n4 ), 'log10' );
is( $n4->max( 1281285 ), 1281285, 'max' );
is( $n4->min( 1281285 ), 1281284, 'min' );
is( $n4->mod( 3 ), 2, 'mod' );
is( $n4->oct, 10, 'oct' );
is( $n4->clone( 3.14159265358979323846 )->round( 4 ), 3.1416, 'Rounding' );
# -0.910377996187395
is( $n4->sin, CORE::sin( $n4 ), 'sin' );
is( $n4->sqrt, CORE::sqrt( $n4 ), 'sqrt' );
# 2.20016257867108
is( $n4->tan, POSIX::tan( $n4 ), 'tan' );
my $pie = $n4->clone( 3.14159265358979323846 );
is( $pie->length, CORE::length( $pie ), 'Number length' );
ok( $n4->is_finite, 'Is finite number' );
ok( $n4->clone( 3.14159265358979323846 )->is_float, 'Is float' );
ok( $n4->is_int, 'Is integer' );
ok( !$n4->is_nan, 'Is NaN' );
ok( $n4->is_positive, 'Is positive number' );
ok( !$n4->is_negative, 'Is negative number' );
ok( $n4->is_normal, 'Is normal number' );
my $inf = Module::Generic::Number->new( 9**9**9, debug => 3 );
isa_ok( $inf, 'Module::Generic::Infinity', 'Infinity Class' );
# diag( "Infinity: $inf" );
# diag( "Is finite? " . $inf->is_finite );
is( "$inf", "Inf", 'Infinity stringified' );
ok( $inf->is_infinite, 'Is infinite' );
ok( !$inf->is_normal, 'Is not normal number' );
is( $inf * 10, 'Inf', 'Infinity overloaded' );
$inf *= -10;
is( $inf, '-Inf', 'Negative infinity overloaded' );
ok( $inf->is_negative, 'Is negative infinity' );
$inf++;
my $inf_p = $inf->clone( 'Inf' );
# diag( "$inf_p + $inf = ", $inf_p + $inf );
is( $inf_p + $inf, 'NaN', 'Infinity to NaN' );
isa_ok( $inf_p + $inf, 'Module::Generic::Nan', 'Infinity to NaN' );
isa_ok( $inf ** 9 / 1000 - $inf, 'Module::Generic::Nan', 'Infinity multiple operations' );
isa_ok( $inf ** 9 / 1000, 'Module::Generic::Infinity', 'Infinity multiple operations (bis)' );
my $v = $inf->abs;
# diag( "Returning value is: $v" );
# diag( "Resulting object class: " . ref( $inf->abs ) . "(" . ref( $v ) . ")" );
# diag( "Return for \$inf->abs->max(10) is " . $inf->abs->max(10) );
# diag( "Return for \$inf->abs->max(10)->floor is " . $inf->abs->max(10)->floor );
isa_ok( $inf->abs->max(10)->floor, 'Module::Generic::Infinity', 'Infinity chaining' );
my $nan = Module::Generic::Number->new( 'NaN' );
isa_ok( $nan, 'Module::Generic::Nan', 'NaN Class' );
is( "$nan", "NaN", 'NaN stringified' );
ok( $nan->is_nan, 'NaN is NaN' );
# diag( "NaN normal: " . $nan->is_normal );
ok( !$nan->is_normal, 'NaN is not normal number' );
# diag( "Min NaN and 10: " . $nan->min( 10 ) );
# diag( POSIX::fmax( 'NaN', 10 ) );
is( $nan->min( 10 ), 10, 'NaN with min' );
is( $nan->max( 10 ), 10, 'NaN with max' );
is( $nan * 10, 'NaN', 'NaN overloaded' );
# diag( "Formatting as bytes '$n4'." );
$n4->debug($DEBUG);
is( $n4->format_bytes, "1${dec_sep}22M", 'Formatting as bytes' );
# diag( "Current value is: $n4" );
is( $n4->format_hex, '0x138D04', 'Formatting as hexadecimal -> 0x138D04' );
is( $n4->format_binary, '100111000110100000100', 'Formatting as binary' );
is( $n4->from_hex( $n4->format_hex ), 1281284, 'Converting from hex' );
is( $n4->from_binary( $n4->format_binary ), 1281284, 'Converting from binary' );
isa_ok( Module::Generic::Number->new( 0 )->as_boolean, 'Module::Generic::Boolean', 'Number to boolean object' );
ok( !Module::Generic::Number->new( 0 )->as_boolean, 'Number to false boolean' );
ok( Module::Generic::Number->new( 2 )->as_boolean, 'Number to true boolean' );
ok( Module::Generic::Number->new( 2 )->as_boolean == 1, 'Number to boolean, checking value' );
is( Module::Generic::Number->new( 74 )->chr, 'J', 'Number to character' );
my $n6 = Module::Generic::Number->new( 10 );
my $s6 = $n6->as_scalar;
isa_ok( $s6, 'Module::Generic::Scalar', 'as_scalar' );
ok( $n6 eq "10", "stringified value" );
my $a = $n6->as_array;
isa_ok( $a, 'Module::Generic::Array', 'as_array => Module::Generic::Array' );
is( $a->[0], 10, 'as_array' );
done_testing();