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   29528 use 5.010001;
  71         273  
2 71     71   402 use strict;
  71         139  
  71         1364  
3 71     71   330 use warnings;
  71         134  
  71         2134  
4              
5             package BSON::Decimal128;
6             # ABSTRACT: BSON type wrapper for Decimal128
7              
8 71     71   352 use version;
  71         134  
  71         292  
9             our $VERSION = 'v1.12.1';
10              
11 71     71   5188 use Carp;
  71         198  
  71         4434  
12 71     71   744 use Math::BigInt;
  71         1629855  
  71         450  
13              
14 71     71   1393501 use Moo;
  71         179  
  71         622  
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   27776 use namespace::clean -except => 'meta';
  71         177  
  71         669  
41              
42             use constant {
43 71         102246 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   24961 };
  71         155  
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   6746 return _bid_to_string( $_[0]->{bytes} );
60             }
61              
62             sub _build_bytes {
63 1965     1965   16176 return _string_to_bid( $_[0]->{value} );
64             }
65              
66             sub BUILD {
67 3172     3172 0 107784 my $self = shift;
68              
69             croak "One and only one of 'value' or 'bytes' must be provided"
70 3172 100       6331 unless 1 == grep { exists $self->{$_} } qw/value bytes/;
  6344         17659  
71              
72             # must check for errors and canonicalize value if provided
73 3171 100       6710 if (exists $self->{value}) {
74 1965         30032 $self->{value} = _bid_to_string( $self->bytes );
75             }
76              
77 3040         17484 return;
78             }
79              
80             sub _bid_to_string {
81 2439     2439   4606 my $bid = shift;
82 2439         6811 my $binary = unpack( "B*", scalar reverse($bid) );
83 2439         3797 my ( $coef, $e );
84              
85             # sign bit
86 2439         4893 my $pos = !substr( $binary, 0, 1 );
87              
88             # detect special values from first 5 bits after sign bit
89 2439         3641 my $special = substr( $binary, 1, 5 );
90 2439 100       5501 if ( $special eq "11111" ) {
91 29         106 return "NaN";
92             }
93 2410 100       4290 if ( $special eq "11110" ) {
94 66 100       277 return $pos ? "Infinity" : "-Infinity";
95             }
96              
97 2344 100       4760 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         23 $coef = "" . Math::BigInt->new( "0b100" . substr( $binary, 17 ) );
101 3         1643 $e = unpack( "n", pack( "B*", "00" . substr( $binary, 3, 14 ) ) ) - BIAS;
102             }
103             else {
104             # Bits: 1*sign 14*exponent 113*significand
105 2341         9295 $coef = "" . Math::BigInt->new( "0b" . substr( $binary, 15 ) );
106 2341         712415 $e = unpack( "n", pack( "B*", "00" . substr( $binary, 1, 14 ) ) ) - BIAS;
107             }
108              
109             # Out of range is treated as zero
110 2344 100       6280 if ( length($coef) > PLIM ) {
111 3         5 $coef = "0";
112             }
113              
114             # Shortcut on zero
115 2344 100 100     8107 if ( $coef == 0 && $e == 0 ) {
116 101 100       453 return $pos ? "0" : "-0";
117             }
118              
119             # convert to scientific form ( e.g. 123E+4 -> 1.23E6 )
120 2243         3745 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     6896 if ( $e > 0 || $adj_exp < -6 ) {
125             # insert decimal if more than one digit
126 1244 100       2526 if ( length($coef) > 1 ) {
127 696         1240 substr( $coef, 1, 0, "." );
128             }
129              
130             return (
131 1244 100       6445 ( $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       2895 return $pos ? $coef : "-$coef"
    100          
138             if $e == 0;
139              
140             # pad with leading zeroes if coefficient is too short
141 694 100       1356 if ( length($coef) < abs($e) ) {
142 348         890 substr( $coef, 0, 0, "0" x ( abs($e) - length($coef) ) );
143             }
144              
145             # maybe coefficient is exact length?
146 694 100       2968 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         444 substr( $coef, $e, 0, "." );
151 237 100       951 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   9468 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   700 sub _erounding { croak("Value '$_[0]' can't be rounded to Decimal128") }
163              
164             sub _string_to_bid {
165 1965     1965   3068 my $s = shift;
166              
167             # Check special values
168 1965 100       5944 return $bidNaN if $s =~ /\A -? NaN \z/ix;
169 1946 100       4867 return $bidPosInf if $s =~ /\A \+?Inf(?:inity)? \z/ix;
170 1921 100       4674 return $bidNegInf if $s =~ /\A -Inf(?:inity)? \z/ix;
171              
172             # Parse string
173 1896         15677 my ( $sign, $mant, $exp ) = $s =~ /\A $decimal_re \z/x;
174 1896 100       5226 $sign = "" unless defined $sign;
175 1896 100 100     7401 $exp = 0 unless defined $exp && length($exp);
176 1896         4544 $exp =~ s{^e}{}i;
177              
178             # Throw error if unparseable
179 1896 100 66     6683 _croak($s) unless length $exp && defined $mant;
180              
181             # Extract sign bit
182 1777 100 66     6262 my $neg = defined($sign) && $sign eq '-' ? "1" : "0";
183              
184             # Remove leading zeroes unless "0."
185 1777         3777 $mant =~ s{^(?:0(?!\.))+}{};
186              
187             # Locate decimal, remove it and adjust the exponent
188 1777         3089 my $dot = index( $mant, "." );
189 1777         3947 $mant =~ s/\.//;
190 1777 100       4246 $exp += $dot - length($mant) if $dot >= 0;
191              
192             # Remove leading zeros from mantissa (after decimal point removed)
193 1777         3147 $mant =~ s/^0+//;
194 1777 100       3521 $mant = "0" unless length $mant;
195              
196             # Apply exact rounding if necessary
197 1777 100       5124 if ( length($mant) > PLIM ) {
    100          
198 17         23 my $plim = PLIM;
199 17         166 $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         25 $mant =~ s{(.*[1-9])(0+)$}{$1};
204 11 100 66     40 $exp += length($2) if defined $2 && length $2;
205             }
206              
207             # Apply clamping if possible
208 1777 100 100     5715 if ( $mant == 0 ) {
    100          
209 482 100       1470 if ( $exp > AEMAX ) {
    100          
210 12         20 $mant = "0";
211 12         37 $exp = AEMAX;
212             }
213             elsif ( $exp < AEMIN ) {
214 8         10 $mant = "0";
215 8         12 $exp = AEMIN;
216             }
217             }
218             elsif ( $exp > AEMAX && $exp - AEMAX <= PLIM - length($mant) ) {
219 76         185 $mant .= "0" x ( $exp - AEMAX );
220 76         118 $exp = AEMAX;
221             }
222              
223             # Throw errors if result won't fit in Decimal128
224 1777 100       3285 _erounding($s) if length($mant) > PLIM;
225 1768 100 100     5117 _erange($s) if $exp > AEMAX || $exp < AEMIN;
226              
227             # Get binary representation of coefficient
228 1765         6440 my $coef = Math::BigInt->new($mant)->as_bin;
229 1765         230933 $coef =~ s/^0b//;
230              
231             # Get 14-bit binary representation of biased exponent
232 1765         7563 my $biased_exp = unpack( "B*", pack( "n", $exp + BIAS ) );
233 1765         3958 substr( $biased_exp, 0, 2, "" );
234              
235             # Choose representation based on coefficient length
236 1765         2494 my $coef_len = length($coef);
237 1765 50       3301 if ( $coef_len <= 113 ) {
    0          
238 1765         4892 substr( $coef, 0, 0, "0" x ( 113 - $coef_len ) );
239 1765         12699 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 3615 return "" . $_[0]->value unless $ENV{BSON_EXTJSON};
264 1519         32138 return { '$numberDecimal' => "" . ($_[0]->value) };
265             }
266              
267             use overload (
268 2     2   511 q{""} => sub { $_[0]->value },
269 71         666 fallback => 1,
270 71     71   600 );
  71         186  
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.1
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) 2019 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__