File Coverage

blib/lib/CLDR/Number/Role/Format.pm
Criterion Covered Total %
statement 174 177 98.3
branch 55 60 91.6
condition 15 22 68.1
subroutine 19 19 100.0
pod 0 2 0.0
total 263 280 93.9


line stmt bran cond sub pod time code
1             package CLDR::Number::Role::Format;
2              
3 16     16   19923 use v5.8.1;
  16         57  
4 16     16   83 use utf8;
  16         27  
  16         185  
5 16     16   332 use Carp;
  16         34  
  16         987  
6 16     16   80 use Scalar::Util qw( looks_like_number );
  16         28  
  16         894  
7 16     16   23046 use Math::BigFloat;
  16         330799  
  16         96  
8 16     16   610185 use Math::Round;
  16         135742  
  16         1098  
9 16     16   464 use CLDR::Number::Constant qw( $N $M $P $C $Q );
  16         29  
  16         2224  
10 16     16   882 use CLDR::Number::Data::Base;
  16         32  
  16         438  
11 16     16   630 use CLDR::Number::Data::System;
  16         27  
  16         369  
12              
13 16     16   81 use Moo::Role;
  16         27  
  16         197  
14              
15             # This role does not have a publicly supported interface and may change in
16             # backward incompatible ways in the future. Please use one of the documented
17             # classes instead.
18              
19             our $VERSION = '0.15';
20              
21             requires qw( BUILD format );
22              
23             with qw( CLDR::Number::Role::Base );
24              
25             has pattern => (
26             is => 'rw',
27             isa => sub {
28             croak "pattern is not defined" if !defined $_[0];
29             },
30             trigger => 1,
31             );
32              
33             has minimum_integer_digits => (
34             is => 'rw',
35             isa => sub {
36             croak "minimum_integer_digits '$_[0]' is invalid"
37             if defined $_[0] && !looks_like_number $_[0];
38             },
39             );
40              
41             has maximum_integer_digits => (
42             is => 'rw',
43             isa => sub {
44             croak "maximum_integer_digits '$_[0]' is invalid"
45             if defined $_[0] && !looks_like_number $_[0];
46             },
47             );
48              
49             has minimum_fraction_digits => (
50             is => 'rw',
51             isa => sub {
52             croak "minimum_fraction_digits '$_[0]' is invalid"
53             if defined $_[0] && !looks_like_number $_[0];
54             },
55             trigger => sub {
56             my ($self, $min) = @_;
57             return unless defined $self->maximum_fraction_digits;
58             return if $min <= $self->maximum_fraction_digits;
59             $self->{maximum_fraction_digits} = $min;
60             },
61             );
62              
63             has maximum_fraction_digits => (
64             is => 'rw',
65             isa => sub {
66             croak "maximum_fraction_digits '$_[0]' is invalid"
67             if defined $_[0] && !looks_like_number $_[0];
68             },
69             trigger => sub {
70             my ($self, $max) = @_;
71             return unless defined $self->minimum_fraction_digits;
72             return if $max >= $self->minimum_fraction_digits;
73             $self->{minimum_fraction_digits} = $max;
74             },
75             );
76              
77             has primary_grouping_size => (
78             is => 'rw',
79             isa => sub {
80             croak "primary_grouping_size '$_[0]' is invalid"
81             if defined $_[0] && !looks_like_number $_[0];
82             },
83             );
84              
85             has secondary_grouping_size => (
86             is => 'rw',
87             isa => sub {
88             croak "secondary_grouping_size '$_[0]' is invalid"
89             if defined $_[0] && !looks_like_number $_[0];
90             },
91             );
92              
93             has rounding_increment => (
94             is => 'rw',
95             isa => sub {
96             croak "rounding_increment '$_[0]' is invalid"
97             if defined $_[0] && !looks_like_number $_[0];
98             },
99             );
100              
101             has _positive_pattern => (
102             is => 'rw',
103             );
104              
105             has _negative_pattern => (
106             is => 'rw',
107             );
108              
109             before BUILD => sub {
110             my ($self) = @_;
111              
112             return if $self->_has_init_arg('locale');
113              
114             $self->_build_pattern;
115             };
116              
117             after _trigger_locale => sub {
118             my ($self) = @_;
119              
120             $self->_build_pattern;
121             };
122              
123             sub _build_pattern {
124 136     136   240 my ($self) = @_;
125              
126 136         531 $self->_set_unless_init_arg(
127             pattern => $self->_get_data(pattern => $self->_pattern_type)
128             );
129             }
130              
131             sub _trigger_pattern {
132 244     244   2843 my ($self, $input_pattern) = @_;
133              
134 244         356 my $cache = $CLDR::Number::Data::Base::CACHE;
135 244 100 66     1913 if (my $attributes
136             = $cache->{attribute}{$input_pattern}
137             || $cache->{pattern}{$input_pattern}
138             && $cache->{attribute}{ $cache->{pattern}{$input_pattern}[0] }) {
139              
140 138         529 while (my ($attribute, $value) = each %$attributes) {
141 828         22853 $self->_set_unless_init_arg($attribute => $value);
142             }
143              
144 138         4911 my $pattern = $cache->{pattern}{$input_pattern};
145              
146 138   66     829 $self->_positive_pattern(
147             $pattern && $pattern->[1] || $N
148             );
149              
150 138   66     965 $self->_negative_pattern(
151             $pattern && $pattern->[2] || $M . $self->_positive_pattern
152             );
153              
154 138         2630 return;
155             }
156              
157             # temporarily replace escaped quotes
158 106         181 $input_pattern =~ s{''}{$Q}g;
159              
160 106         132 my $internal_pattern = '';
161 106         124 my $canonical_pattern = '';
162 106         110 my $num_subpattern;
163              
164 106         444 while ($input_pattern =~ m{
165             \G (?:
166             ( [^']+ ) # non-quoted text
167             |
168             ' ( [^']+ ) (?: ' | $ ) # quoted text (trailing quote optional)
169             )
170             }xg) {
171 111         210 my $nonquoted = $1;
172 111         143 my $quoted = $2;
173              
174 111 100       220 if (defined $nonquoted) {
    50          
175 102 50 33     5475 if (!defined $num_subpattern && $nonquoted =~ m{
176             ^ ( .*? ) # pre–number pattern
177             ( (?: \* \X )? [@#0-9,.]+ ) # number pattern
178             ( .* ) $ # post–number pattern
179             }x) {
180 102         215 my $prenum = $1;
181 102         137 $num_subpattern = $2;
182 102         161 my $postnum = $3;
183              
184 102         250 $num_subpattern = $self->_process_num_pattern($num_subpattern);
185              
186 102         861 $internal_pattern .= _escape_symbols($prenum . $N . $postnum);
187 102         410 $canonical_pattern .= $prenum . $num_subpattern . $postnum;
188             }
189             else {
190 0         0 $internal_pattern .= _escape_symbols($nonquoted);
191 0         0 $canonical_pattern .= $nonquoted;
192             }
193             }
194             elsif (defined $quoted) {
195 9         17 $internal_pattern .= $quoted;
196 9         36 $canonical_pattern .= "'$quoted'";
197             }
198             }
199              
200 106         327 $internal_pattern =~ s{$Q}{'}g;
201 106         266 $canonical_pattern =~ s{$Q}{''}g;
202              
203 106         278 $self->_positive_pattern($internal_pattern);
204 106         269 $self->_negative_pattern($M . $internal_pattern);
205              
206             # hashref instead of attribute method so wo don’t retrigger this trigger
207 106         2102 $self->{pattern} = $canonical_pattern;
208             }
209              
210             sub _validate_number {
211 312     312   522 my ($self, $method, $num) = @_;
212              
213 312 100       667 if (!defined $num) {
214 6         7 carp qq[Use of uninitialized value in ${\ref $self}::$method];
  6         90  
215 6         3813 return undef;
216             }
217              
218 306 100       879 if (!looks_like_number $num) {
219 14         24 carp qq[Argument "$num" isn't numeric in ${\ref $self}::$method];
  14         157  
220 16     16   27269 no warnings;
  16         39  
  16         23867  
221 14         8577 $num += 0;
222             }
223              
224 306         689 return $num;
225             }
226              
227             my $INF = 9**9**9;
228              
229             sub _format_number {
230 273     273   418 my ($self, $num) = @_;
231 273         312 my ($format, $num_format);
232              
233 273 100       609 if ($num < 0) {
234 30         163 my $pattern = $self->_negative_pattern;
235 30         172 $pattern =~ s{$M}{$self->minus_sign}e;
  29         96  
236 30         63 $format = $pattern;
237             }
238             else {
239 243         570 $format = $self->_positive_pattern;
240             }
241              
242 273 100 100     1551 if ($num == $INF || $num == -$INF) {
    100          
243 17         32 $num_format = $self->infinity;
244             }
245             elsif (!defined($num <=> $INF)) {
246 9         18 $num_format = $self->nan;
247             }
248             else {
249 247         313 my $rounded;
250              
251 247 100       748 if ($self->rounding_increment) {
252             # TODO: round half to even
253 23         629 $rounded = Math::Round::nearest(
254             $self->rounding_increment,
255             abs $num
256             );
257             }
258             else {
259             # round half to even
260 224         6440 $rounded = Math::BigFloat->new($num)->ffround(
261             -$self->maximum_fraction_digits,
262             'even'
263             )->babs->bstr;
264             }
265              
266 247         62774 my ($int, $frac) = split /\./, $rounded;
267 247 100       632 if (!defined $frac) {
268 50         83 $frac = '';
269             }
270              
271 247 100       709 if (my $primary_group = $self->primary_grouping_size) {
272 215         6068 my $group_sign = $self->group_sign;
273 215   66     577 my $other_groups = $self->secondary_grouping_size || $primary_group;
274              
275 215         6717 $int =~ s{ (?
276              
277 215         309 while (1) {
278 231 100       2808 last if $int !~ s{
279             (?
280             (?
281             (?= .{$other_groups} \Q$group_sign\E )
282             }{$group_sign}x;
283             }
284             }
285              
286 247   50     1571 my $int_pad = $self->minimum_integer_digits - (length $int || 0);
287 247 100       6649 if ($int_pad > 0) {
288 5         15 $int = 0 x $int_pad . $int;
289             }
290              
291 247   100     677 my $frac_pad = $self->minimum_fraction_digits - (length $frac || 0);
292 247 100       6620 if ($frac_pad > 0) {
    100          
293 2         6 $frac .= 0 x $frac_pad;
294             }
295             elsif ($frac_pad < 0) {
296 100         157 my $truncate_size = abs $frac_pad;
297 100         707 $frac =~ s{ 0{1,$truncate_size} $ }{}x;
298             }
299              
300 247         436 $num_format = $int;
301              
302 247 100       539 if (length $frac) {
303 162         441 $num_format .= $self->decimal_sign . $frac;
304             }
305              
306 247 100       696 if ($self->numbering_system ne 'latn') {
307             my $digits = $CLDR::Number::Data::System::DATA->{
308 9         238 $self->numbering_system
309             };
310              
311 9         333 $num_format =~ s{ ( [0-9] ) }{$digits->[$1]}xg;
312             }
313             }
314              
315 273         14468 $format =~ s{$N}{$num_format};
316              
317 273         1188 return $format;
318             }
319              
320             sub _process_num_pattern {
321 102     102   159 my ($self, $num_pattern) = @_;
322              
323 102         199 for ($num_pattern) {
324 102         158 s{ \. $ }{}x; # no trailing decimal sign
325 102         288 s{ (?: ^ | \# ) (?= \. ) }{0}x; # at least one minimum integer digit
326              
327             # calculate grouping sizes
328 102         292 my ($secondary, $primary) = map { length } m{
  62         111  
329             , ( [^,]* ) # primary
330             , ( [^,.]* ) # secondary
331             (?: \. | $ )
332             }x;
333              
334 102 100       252 if (!defined $primary) {
    100          
    100          
335 71         181 ($primary) = map { length } m{
  19         55  
336             , ( [^,.]* ) # primary only
337             (?: \. | $ )
338             }x;
339             }
340             elsif ($primary == 0) {
341 6         10 $primary = $secondary;
342 6         8 $secondary = undef;
343             }
344             elsif ($primary == $secondary) {
345 4         6 $secondary = undef;
346             }
347              
348 102         197 tr{,}{}d; # temporarily remove groups
349              
350 102 100       272 if (!m{ \. }x) {
351 80         331 s{ (?: ^ | \# ) $ }{0}x; # at least one minimum integer digit
352             }
353              
354 102 50       335 if (!$self->_has_init_arg('minimum_integer_digits')) {
355 102         349 my ($min_int) = m{ ( [0-9,]+ ) (?= \. | $ ) }x;
356 102         343 $self->minimum_integer_digits(length $min_int);
357             }
358              
359 102 100       855 if ($primary) {
360 42         503 s{ (?= .{$primary} (?: \. | $ ) ) }{,}x; # add primary group
361 42         147 $self->_set_unless_init_arg(primary_grouping_size => $primary);
362              
363 42 100       347 if ($secondary) {
364 19         128 s{ (?= .{$secondary} , ) }{,}x; # add secondary group
365 19         57 $self->_set_unless_init_arg(
366             secondary_grouping_size => $secondary
367             );
368             }
369             else {
370 23         68 $self->_set_unless_init_arg(secondary_grouping_size => 0);
371             }
372             }
373             else {
374 60         193 $self->_set_unless_init_arg(primary_grouping_size => 0);
375 60         561 $self->_set_unless_init_arg(secondary_grouping_size => 0);
376             }
377              
378 102         835 s{ ^ \#+ (?= [#0-9] ) }{}x; # no leading multiple #s
379 102         196 s{ ^ (?= , ) }{#}x; # leading # before group
380              
381 102 100       351 if (my ($max, $min) = m{ \. ( ( [0-9]* ) \#* ) }x) {
382 22         88 $self->_set_unless_init_arg(minimum_fraction_digits => length $min);
383 22         915 $self->_set_unless_init_arg(maximum_fraction_digits => length $max);
384             }
385             else {
386 80         225 $self->_set_unless_init_arg(minimum_fraction_digits => 0);
387 80         3329 $self->_set_unless_init_arg(maximum_fraction_digits => 0);
388             }
389              
390 102 50       4324 if (!$self->_has_init_arg('rounding_increment')) {
391 102 50       487 if (my ($round_inc) = m{ (
392             (?: [1-9] [0-9,]* | 0 ) # integer
393             (?= \. | $ )
394             (?: \. [0-9]* [1-9] )? # fraction
395             ) }x) {
396 102         308 $self->rounding_increment($round_inc);
397             }
398             else {
399 0         0 $self->rounding_increment(0);
400             }
401             }
402             }
403              
404 102         938 return $num_pattern;
405             }
406              
407             sub _escape_symbols {
408 102     102   138 my ($pattern) = @_;
409              
410 102         182 for ($pattern) {
411 102         167 s{%}{$P};
412 102         164 s{¤}{$C};
413 102         190 s{-}{$M};
414             }
415              
416 102         182 return $pattern;
417             }
418              
419             sub at_least {
420 6     6 0 143 my ($self, $num) = @_;
421 6         24 my $pattern = $self->_get_data(pattern => 'at_least');
422              
423 6         27 $num = $self->_validate_number(at_least => $num);
424 6 100       21 return undef unless defined $num;
425              
426 5         22 $num = $self->format($num);
427 5         21 $pattern =~ s{ \{ 0 \} }{$num}x;
428              
429 5         24 return $pattern;
430             }
431              
432             sub range {
433 15     15 0 322 my ($self, @nums) = @_;
434 15         64 my $pattern = $self->_get_data(pattern => 'range');
435              
436 15         37 for my $i (0, 1) {
437 29         87 my $num = $self->_validate_number(range => $nums[$i]);
438 29 100       78 return undef unless defined $num;
439              
440 27         94 $num = $self->format($num);
441 27         392 $pattern =~ s{ \{ $i \} }{$num}x;
442             }
443              
444 13         78 return $pattern;
445             }
446              
447             1;