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   26628 use 5.010001;
  71         260  
2 71     71   341 use strict;
  71         131  
  71         1225  
3 71     71   287 use warnings;
  71         117  
  71         4477  
4              
5             package BSON::Decimal128;
6             # ABSTRACT: BSON type wrapper for Decimal128
7              
8 71     71   329 use version;
  71         138  
  71         280  
9             our $VERSION = 'v1.12.0';
10              
11 71     71   4887 use Carp;
  71         169  
  71         4095  
12 71     71   707 use Math::BigInt;
  71         1539269  
  71         381  
13              
14 71     71   1294276 use Moo;
  71         197  
  71         597  
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   26002 use namespace::clean -except => 'meta';
  71         156  
  71         658  
41              
42             use constant {
43 71         94970 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   23247 };
  71         151  
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   5597 return _bid_to_string( $_[0]->{bytes} );
60             }
61              
62             sub _build_bytes {
63 1965     1965   13098 return _string_to_bid( $_[0]->{value} );
64             }
65              
66             sub BUILD {
67 3172     3172 0 95147 my $self = shift;
68              
69             croak "One and only one of 'value' or 'bytes' must be provided"
70 3172 100       5186 unless 1 == grep { exists $self->{$_} } qw/value bytes/;
  6344         15174  
71              
72             # must check for errors and canonicalize value if provided
73 3171 100       5807 if (exists $self->{value}) {
74 1965         26600 $self->{value} = _bid_to_string( $self->bytes );
75             }
76              
77 3040         16176 return;
78             }
79              
80             sub _bid_to_string {
81 2439     2439   3718 my $bid = shift;
82 2439         5965 my $binary = unpack( "B*", scalar reverse($bid) );
83 2439         3456 my ( $coef, $e );
84              
85             # sign bit
86 2439         3934 my $pos = !substr( $binary, 0, 1 );
87              
88             # detect special values from first 5 bits after sign bit
89 2439         3180 my $special = substr( $binary, 1, 5 );
90 2439 100       4509 if ( $special eq "11111" ) {
91 29         88 return "NaN";
92             }
93 2410 100       3439 if ( $special eq "11110" ) {
94 66 100       216 return $pos ? "Infinity" : "-Infinity";
95             }
96              
97 2344 100       4101 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         19 $coef = "" . Math::BigInt->new( "0b100" . substr( $binary, 17 ) );
101 3         1348 $e = unpack( "n", pack( "B*", "00" . substr( $binary, 3, 14 ) ) ) - BIAS;
102             }
103             else {
104             # Bits: 1*sign 14*exponent 113*significand
105 2341         8341 $coef = "" . Math::BigInt->new( "0b" . substr( $binary, 15 ) );
106 2341         589459 $e = unpack( "n", pack( "B*", "00" . substr( $binary, 1, 14 ) ) ) - BIAS;
107             }
108              
109             # Out of range is treated as zero
110 2344 100       5386 if ( length($coef) > PLIM ) {
111 3         6 $coef = "0";
112             }
113              
114             # Shortcut on zero
115 2344 100 100     6532 if ( $coef == 0 && $e == 0 ) {
116 101 100       375 return $pos ? "0" : "-0";
117             }
118              
119             # convert to scientific form ( e.g. 123E+4 -> 1.23E6 )
120 2243         3368 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     5950 if ( $e > 0 || $adj_exp < -6 ) {
125             # insert decimal if more than one digit
126 1244 100       2319 if ( length($coef) > 1 ) {
127 696         1168 substr( $coef, 1, 0, "." );
128             }
129              
130             return (
131 1244 100       5699 ( $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       2466 return $pos ? $coef : "-$coef"
    100          
138             if $e == 0;
139              
140             # pad with leading zeroes if coefficient is too short
141 694 100       1207 if ( length($coef) < abs($e) ) {
142 348         812 substr( $coef, 0, 0, "0" x ( abs($e) - length($coef) ) );
143             }
144              
145             # maybe coefficient is exact length?
146 694 100       2485 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         383 substr( $coef, $e, 0, "." );
151 237 100       877 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   9391 sub _croak { croak("Couldn't parse '$_[0]' as valid Decimal128") }
159              
160 3     3   227 sub _erange { croak("Value '$_[0]' is out of range for Decimal128") }
161              
162 9     9   715 sub _erounding { croak("Value '$_[0]' can't be rounded to Decimal128") }
163              
164             sub _string_to_bid {
165 1965     1965   2841 my $s = shift;
166              
167             # Check special values
168 1965 100       4884 return $bidNaN if $s =~ /\A -? NaN \z/ix;
169 1946 100       4144 return $bidPosInf if $s =~ /\A \+?Inf(?:inity)? \z/ix;
170 1921 100       3760 return $bidNegInf if $s =~ /\A -Inf(?:inity)? \z/ix;
171              
172             # Parse string
173 1896         13587 my ( $sign, $mant, $exp ) = $s =~ /\A $decimal_re \z/x;
174 1896 100       4074 $sign = "" unless defined $sign;
175 1896 100 100     6212 $exp = 0 unless defined $exp && length($exp);
176 1896         4239 $exp =~ s{^e}{}i;
177              
178             # Throw error if unparseable
179 1896 100 66     5580 _croak($s) unless length $exp && defined $mant;
180              
181             # Extract sign bit
182 1777 100 66     5085 my $neg = defined($sign) && $sign eq '-' ? "1" : "0";
183              
184             # Remove leading zeroes unless "0."
185 1777         3007 $mant =~ s{^(?:0(?!\.))+}{};
186              
187             # Locate decimal, remove it and adjust the exponent
188 1777         2908 my $dot = index( $mant, "." );
189 1777         3272 $mant =~ s/\.//;
190 1777 100       3820 $exp += $dot - length($mant) if $dot >= 0;
191              
192             # Remove leading zeros from mantissa (after decimal point removed)
193 1777         2655 $mant =~ s/^0+//;
194 1777 100       2987 $mant = "0" unless length $mant;
195              
196             # Apply exact rounding if necessary
197 1777 100       4061 if ( length($mant) > PLIM ) {
    100          
198 17         24 my $plim = PLIM;
199 17         133 $mant =~ s{(.{$plim})(0+)$}{$1};
200 17 100 66     80 $exp += length($2) if defined $2 && length $2;
201             }
202             elsif ( $exp < AEMIN ) {
203 11         21 $mant =~ s{(.*[1-9])(0+)$}{$1};
204 11 100 66     39 $exp += length($2) if defined $2 && length $2;
205             }
206              
207             # Apply clamping if possible
208 1777 100 100     5002 if ( $mant == 0 ) {
    100          
209 482 100       1033 if ( $exp > AEMAX ) {
    100          
210 12         15 $mant = "0";
211 12         16 $exp = AEMAX;
212             }
213             elsif ( $exp < AEMIN ) {
214 8         12 $mant = "0";
215 8         10 $exp = AEMIN;
216             }
217             }
218             elsif ( $exp > AEMAX && $exp - AEMAX <= PLIM - length($mant) ) {
219 76         197 $mant .= "0" x ( $exp - AEMAX );
220 76         125 $exp = AEMAX;
221             }
222              
223             # Throw errors if result won't fit in Decimal128
224 1777 100       2771 _erounding($s) if length($mant) > PLIM;
225 1768 100 100     4828 _erange($s) if $exp > AEMAX || $exp < AEMIN;
226              
227             # Get binary representation of coefficient
228 1765         5100 my $coef = Math::BigInt->new($mant)->as_bin;
229 1765         189301 $coef =~ s/^0b//;
230              
231             # Get 14-bit binary representation of biased exponent
232 1765         6287 my $biased_exp = unpack( "B*", pack( "n", $exp + BIAS ) );
233 1765         3688 substr( $biased_exp, 0, 2, "" );
234              
235             # Choose representation based on coefficient length
236 1765         2347 my $coef_len = length($coef);
237 1765 50       3144 if ( $coef_len <= 113 ) {
    0          
238 1765         4126 substr( $coef, 0, 0, "0" x ( 113 - $coef_len ) );
239 1765         10680 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 3495 return "" . $_[0]->value unless $ENV{BSON_EXTJSON};
264 1519         28916 return { '$numberDecimal' => "" . ($_[0]->value) };
265             }
266              
267             use overload (
268 2     2   641 q{""} => sub { $_[0]->value },
269 71         662 fallback => 1,
270 71     71   587 );
  71         160  
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.0
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__