File Coverage

blib/lib/BSON/Decimal128.pm
Criterion Covered Total %
statement 112 115 97.3
branch 79 82 96.3
condition 23 27 85.1
subroutine 20 20 100.0
pod 1 2 50.0
total 235 246 95.5


line stmt bran cond sub pod time code
1 71     71   28563 use 5.010001;
  71         242  
2 71     71   373 use strict;
  71         117  
  71         1429  
3 71     71   305 use warnings;
  71         121  
  71         2137  
4              
5             package BSON::Decimal128;
6             # ABSTRACT: BSON type wrapper for Decimal128
7              
8 71     71   336 use version;
  71         106  
  71         380  
9             our $VERSION = 'v1.12.2';
10              
11 71     71   5164 use Carp;
  71         139  
  71         5369  
12 71     71   661 use Math::BigInt;
  71         1685766  
  71         395  
13              
14 71     71   1397512 use Moo;
  71         166  
  71         556  
15              
16             #pod =attr value
17             #pod
18             #pod The Decimal128 value represented as string. If not provided, it will be
19             #pod generated from the C attribute on demand.
20             #pod
21             #pod =cut
22              
23             has 'value' => (
24             is => 'lazy',
25             );
26              
27             #pod =attr bytes
28             #pod
29             #pod The Decimal128 value represented in L
30             #pod Decimal|https://en.wikipedia.org/wiki/Binary_Integer_Decimal> (BID) format.
31             #pod If not provided, it will be generated from the C attribute on
32             #pod demand.
33             #pod
34             #pod =cut
35              
36             has 'bytes' => (
37             is => 'lazy',
38             );
39              
40 71     71   25085 use namespace::clean -except => 'meta';
  71         159  
  71         620  
41              
42             use constant {
43 71         100503 PLIM => 34, # precision limit, i.e. max coefficient chars
44             EMAX => 6144, # for 9.999999999999999999999999999999999E+6144
45             EMIN => -6143, # for 1.000000000000000000000000000000000E-6143
46             AEMAX => 6111, # EMAX - (PLIM - 1); largest encodable exponent
47             AEMIN => -6176, # EMIN - (PLIM - 1); smallest encodable exponent
48             BIAS => 6176, # offset for encoding exponents
49 71     71   22565 };
  71         145  
50              
51             my $digits = qr/[0-9]+/;
52             my $decimal_re = qr{
53             ( [-+]? ) # maybe a sign
54             ( (?:$digits \. $digits? ) | (?: \.? $digits ) ) # decimal-part
55             ( (?:e [-+]? $digits)? ) # maybe exponent
56             }ix;
57              
58             sub _build_value {
59 605     605   5988 return _bid_to_string( $_[0]->{bytes} );
60             }
61              
62             sub _build_bytes {
63 1965     1965   16832 return _string_to_bid( $_[0]->{value} );
64             }
65              
66             sub BUILD {
67 3172     3172 0 102824 my $self = shift;
68              
69             croak "One and only one of 'value' or 'bytes' must be provided"
70 3172 100       5251 unless 1 == grep { exists $self->{$_} } qw/value bytes/;
  6344         16585  
71              
72             # must check for errors and canonicalize value if provided
73 3171 100       5796 if (exists $self->{value}) {
74 1965         28518 $self->{value} = _bid_to_string( $self->bytes );
75             }
76              
77 3040         18112 return;
78             }
79              
80             sub _bid_to_string {
81 2439     2439   3894 my $bid = shift;
82 2439         6185 my $binary = unpack( "B*", scalar reverse($bid) );
83 2439         3860 my ( $coef, $e );
84              
85             # sign bit
86 2439         4205 my $pos = !substr( $binary, 0, 1 );
87              
88             # detect special values from first 5 bits after sign bit
89 2439         3255 my $special = substr( $binary, 1, 5 );
90 2439 100       4618 if ( $special eq "11111" ) {
91 29         91 return "NaN";
92             }
93 2410 100       3912 if ( $special eq "11110" ) {
94 66 100       211 return $pos ? "Infinity" : "-Infinity";
95             }
96              
97 2344 100       4176 if ( substr( $binary, 1, 2 ) eq '11' ) {
98             # Bits: 1*sign 2*ignored 14*exponent 111*significand.
99             # Implicit 0b100 prefix in significand.
100 3         20 $coef = "" . Math::BigInt->new( "0b100" . substr( $binary, 17 ) );
101 3         1339 $e = unpack( "n", pack( "B*", "00" . substr( $binary, 3, 14 ) ) ) - BIAS;
102             }
103             else {
104             # Bits: 1*sign 14*exponent 113*significand
105 2341         9022 $coef = "" . Math::BigInt->new( "0b" . substr( $binary, 15 ) );
106 2341         648984 $e = unpack( "n", pack( "B*", "00" . substr( $binary, 1, 14 ) ) ) - BIAS;
107             }
108              
109             # Out of range is treated as zero
110 2344 100       5836 if ( length($coef) > PLIM ) {
111 3         5 $coef = "0";
112             }
113              
114             # Shortcut on zero
115 2344 100 100     7384 if ( $coef == 0 && $e == 0 ) {
116 101 100       362 return $pos ? "0" : "-0";
117             }
118              
119             # convert to scientific form ( e.g. 123E+4 -> 1.23E6 )
120 2243         3605 my $adj_exp = $e + length($coef) - 1;
121             # warn "# XXX COEF: $coef; EXP: $e; AEXP: $adj_exp\n";
122              
123             # exponential notation
124 2243 100 100     6471 if ( $e > 0 || $adj_exp < -6 ) {
125             # insert decimal if more than one digit
126 1244 100       2439 if ( length($coef) > 1 ) {
127 696         1271 substr( $coef, 1, 0, "." );
128             }
129              
130             return (
131 1244 100       6343 ( $pos ? "" : "-" ) . $coef . "E" . ( $adj_exp >= 0 ? "+" : "" ) . $adj_exp );
    100          
132             }
133              
134             # not exponential notation (integers or small negative exponents)
135             else {
136             # e == 0 means integer
137 999 100       13027 return $pos ? $coef : "-$coef"
    100          
138             if $e == 0;
139              
140             # pad with leading zeroes if coefficient is too short
141 694 100       1277 if ( length($coef) < abs($e) ) {
142 348         976 substr( $coef, 0, 0, "0" x ( abs($e) - length($coef) ) );
143             }
144              
145             # maybe coefficient is exact length?
146 694 100       2579 return $pos ? "0.$coef" : "-0.$coef"
    100          
147             if length($coef) == abs($e);
148              
149             # otherwise length(coef) > abs($e), so insert dot after first digit
150 237         379 substr( $coef, $e, 0, "." );
151 237 100       900 return $pos ? $coef : "-$coef";
152             }
153             }
154              
155             my ( $bidNaN, $bidPosInf, $bidNegInf ) =
156             map { scalar reverse pack( "B*", $_ . ( "0" x 118 ) ) } qw/ 011111 011110 111110 /;
157              
158 119     119   9514 sub _croak { croak("Couldn't parse '$_[0]' as valid Decimal128") }
159              
160 3     3   235 sub _erange { croak("Value '$_[0]' is out of range for Decimal128") }
161              
162 9     9   713 sub _erounding { croak("Value '$_[0]' can't be rounded to Decimal128") }
163              
164             sub _string_to_bid {
165 1965     1965   2776 my $s = shift;
166              
167             # Check special values
168 1965 100       5296 return $bidNaN if $s =~ /\A -? NaN \z/ix;
169 1946 100       5147 return $bidPosInf if $s =~ /\A \+?Inf(?:inity)? \z/ix;
170 1921 100       4598 return $bidNegInf if $s =~ /\A -Inf(?:inity)? \z/ix;
171              
172             # Parse string
173 1896         15171 my ( $sign, $mant, $exp ) = $s =~ /\A $decimal_re \z/x;
174 1896 100       4818 $sign = "" unless defined $sign;
175 1896 100 100     7806 $exp = 0 unless defined $exp && length($exp);
176 1896         4340 $exp =~ s{^e}{}i;
177              
178             # Throw error if unparseable
179 1896 100 66     5721 _croak($s) unless length $exp && defined $mant;
180              
181             # Extract sign bit
182 1777 100 66     5970 my $neg = defined($sign) && $sign eq '-' ? "1" : "0";
183              
184             # Remove leading zeroes unless "0."
185 1777         3735 $mant =~ s{^(?:0(?!\.))+}{};
186              
187             # Locate decimal, remove it and adjust the exponent
188 1777         3557 my $dot = index( $mant, "." );
189 1777         3460 $mant =~ s/\.//;
190 1777 100       5130 $exp += $dot - length($mant) if $dot >= 0;
191              
192             # Remove leading zeros from mantissa (after decimal point removed)
193 1777         3161 $mant =~ s/^0+//;
194 1777 100       4106 $mant = "0" unless length $mant;
195              
196             # Apply exact rounding if necessary
197 1777 100       5087 if ( length($mant) > PLIM ) {
    100          
198 17         29 my $plim = PLIM;
199 17         141 $mant =~ s{(.{$plim})(0+)$}{$1};
200 17 100 66     84 $exp += length($2) if defined $2 && length $2;
201             }
202             elsif ( $exp < AEMIN ) {
203 11         19 $mant =~ s{(.*[1-9])(0+)$}{$1};
204 11 100 66     41 $exp += length($2) if defined $2 && length $2;
205             }
206              
207             # Apply clamping if possible
208 1777 100 100     5217 if ( $mant == 0 ) {
    100          
209 482 100       1149 if ( $exp > AEMAX ) {
    100          
210 12         20 $mant = "0";
211 12         14 $exp = AEMAX;
212             }
213             elsif ( $exp < AEMIN ) {
214 8         11 $mant = "0";
215 8         12 $exp = AEMIN;
216             }
217             }
218             elsif ( $exp > AEMAX && $exp - AEMAX <= PLIM - length($mant) ) {
219 76         195 $mant .= "0" x ( $exp - AEMAX );
220 76         108 $exp = AEMAX;
221             }
222              
223             # Throw errors if result won't fit in Decimal128
224 1777 100       3028 _erounding($s) if length($mant) > PLIM;
225 1768 100 100     8357 _erange($s) if $exp > AEMAX || $exp < AEMIN;
226              
227             # Get binary representation of coefficient
228 1765         6773 my $coef = Math::BigInt->new($mant)->as_bin;
229 1765         205323 $coef =~ s/^0b//;
230              
231             # Get 14-bit binary representation of biased exponent
232 1765         7173 my $biased_exp = unpack( "B*", pack( "n", $exp + BIAS ) );
233 1765         3662 substr( $biased_exp, 0, 2, "" );
234              
235             # Choose representation based on coefficient length
236 1765         2396 my $coef_len = length($coef);
237 1765 50       3131 if ( $coef_len <= 113 ) {
    0          
238 1765         4491 substr( $coef, 0, 0, "0" x ( 113 - $coef_len ) );
239 1765         11232 return scalar reverse pack( "B*", $neg . $biased_exp . $coef );
240             }
241             elsif ( $coef_len <= 114 ) {
242 0         0 substr( $coef, 0, 3, "" );
243 0         0 return scalar reverse pack( "B*", $neg . "11" . $biased_exp . $coef );
244             }
245             else {
246 0         0 _erange($s);
247             }
248             }
249              
250             #pod =method TO_JSON
251             #pod
252             #pod Returns the value as a string.
253             #pod
254             #pod If the C option is true, it will instead
255             #pod be compatible with MongoDB's L
256             #pod format, which represents it as a document as follows:
257             #pod
258             #pod {"$numberDecimal" : "2.23372036854775807E+57"}
259             #pod
260             #pod =cut
261              
262             sub TO_JSON {
263 1522 100   1522 1 3255 return "" . $_[0]->value unless $ENV{BSON_EXTJSON};
264 1519         30112 return { '$numberDecimal' => "" . ($_[0]->value) };
265             }
266              
267             use overload (
268 2     2   568 q{""} => sub { $_[0]->value },
269 71         849 fallback => 1,
270 71     71   897 );
  71         185  
271              
272             1;
273              
274             =pod
275              
276             =encoding UTF-8
277              
278             =head1 NAME
279              
280             BSON::Decimal128 - BSON type wrapper for Decimal128
281              
282             =head1 VERSION
283              
284             version v1.12.2
285              
286             =head1 SYNOPSIS
287              
288             use BSON::Types ':all';
289              
290             # string representation
291             $decimal = bson_decimal128( "1.23456789E+1000" );
292              
293             # binary representation in BID format
294             $decimal = BSON::Decimal128->new( bytes => $bid )
295              
296             =head1 DESCRIPTION
297              
298             This module provides a BSON type wrapper for Decimal128 values.
299              
300             It may be initialized with either a numeric value in string form, or
301             with a binary Decimal128 representation (16 bytes), but not both.
302              
303             Initialization from a string will throw an error if the string cannot be
304             parsed as a Decimal128 or if the resulting number would not fit into 128
305             bits. If required, clamping or exact rounding will be applied to try to
306             fit the value into 128 bits.
307              
308             =head1 ATTRIBUTES
309              
310             =head2 value
311              
312             The Decimal128 value represented as string. If not provided, it will be
313             generated from the C attribute on demand.
314              
315             =head2 bytes
316              
317             The Decimal128 value represented in L
318             Decimal|https://en.wikipedia.org/wiki/Binary_Integer_Decimal> (BID) format.
319             If not provided, it will be generated from the C attribute on
320             demand.
321              
322             =head1 METHODS
323              
324             =head2 TO_JSON
325              
326             Returns the value as a string.
327              
328             If the C option is true, it will instead
329             be compatible with MongoDB's L
330             format, which represents it as a document as follows:
331              
332             {"$numberDecimal" : "2.23372036854775807E+57"}
333              
334             =for Pod::Coverage BUILD
335              
336             =head1 OVERLOADING
337              
338             The stringification operator (C<"">) is overloaded to return a (normalized)
339             string representation. Fallback overloading is enabled.
340              
341             =head1 AUTHORS
342              
343             =over 4
344              
345             =item *
346              
347             David Golden
348              
349             =item *
350              
351             Stefan G.
352              
353             =back
354              
355             =head1 COPYRIGHT AND LICENSE
356              
357             This software is Copyright (c) 2020 by Stefan G. and MongoDB, Inc.
358              
359             This is free software, licensed under:
360              
361             The Apache License, Version 2.0, January 2004
362              
363             =cut
364              
365             __END__