File Coverage

blib/lib/CLDR/Number/Role/Format.pm
Criterion Covered Total %
statement 175 178 98.3
branch 55 60 91.6
condition 18 25 72.0
subroutine 19 19 100.0
pod 0 2 0.0
total 267 284 94.0


line stmt bran cond sub pod time code
1             package CLDR::Number::Role::Format;
2              
3 16     16   12867 use v5.8.1;
  16         37  
4 16     16   74 use utf8;
  16         18  
  16         64  
5 16     16   419 use Carp;
  16         18  
  16         769  
6 16     16   59 use Scalar::Util qw( looks_like_number );
  16         20  
  16         642  
7 16     16   13373 use Math::BigFloat;
  16         222490  
  16         64  
8 16     16   411940 use Math::Round;
  16         93060  
  16         850  
9 16     16   76 use CLDR::Number::Constant qw( $N $M $P $C $Q );
  16         23  
  16         1676  
10 16     16   553 use CLDR::Number::Data::Base;
  16         23  
  16         354  
11 16     16   376 use CLDR::Number::Data::System;
  16         16  
  16         263  
12              
13 16     16   52 use Moo::Role;
  16         17  
  16         123  
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.19';
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 138     138   128 my ($self) = @_;
125              
126 138         394 $self->_set_unless_init_arg(
127             pattern => $self->_get_data(pattern => $self->_pattern_type)
128             );
129             }
130              
131             sub _trigger_pattern {
132 246     246   2237 my ($self, $input_pattern) = @_;
133              
134 246         258 my $cache = $CLDR::Number::Data::Base::CACHE;
135 246 100 66     1395 if (my $attributes
136             = $cache->{attribute}{$input_pattern}
137             || $cache->{pattern}{$input_pattern}
138             && $cache->{attribute}{ $cache->{pattern}{$input_pattern}[0] }) {
139              
140 140         404 while (my ($attribute, $value) = each %$attributes) {
141 840         18506 $self->_set_unless_init_arg($attribute => $value);
142             }
143              
144 140         2230 my $pattern = $cache->{pattern}{$input_pattern};
145              
146 140   66     656 $self->_positive_pattern(
147             $pattern && $pattern->[1] || $N
148             );
149              
150 140   66     740 $self->_negative_pattern(
151             $pattern && $pattern->[2] || $M . $self->_positive_pattern
152             );
153              
154 140         2154 return;
155             }
156              
157             # temporarily replace escaped quotes
158 106         142 $input_pattern =~ s{''}{$Q}g;
159              
160 106         105 my $internal_pattern = '';
161 106         84 my $canonical_pattern = '';
162 106         79 my $num_subpattern;
163              
164 106         374 while ($input_pattern =~ m{
165             \G (?:
166             ( [^']+ ) # non-quoted text
167             |
168             ' ( [^']+ ) (?: ' | $ ) # quoted text (trailing quote optional)
169             )
170             }xg) {
171 111         155 my $nonquoted = $1;
172 111         122 my $quoted = $2;
173              
174 111 100       180 if (defined $nonquoted) {
    50          
175 102 50 33     620 if (!defined $num_subpattern && $nonquoted =~ m{
176             ^ ( .*? ) # pre–number pattern
177             ( (?: \* \X )? [@#0-9,.]+ ) # number pattern
178             ( .* ) $ # post–number pattern
179             }x) {
180 102         124 my $prenum = $1;
181 102         106 $num_subpattern = $2;
182 102         89 my $postnum = $3;
183              
184 102         181 $num_subpattern = $self->_process_num_pattern($num_subpattern);
185              
186 102         247 $internal_pattern .= _escape_symbols($prenum . $N . $postnum);
187 102         323 $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         12 $internal_pattern .= $quoted;
196 9         34 $canonical_pattern .= "'$quoted'";
197             }
198             }
199              
200 106         258 $internal_pattern =~ s{$Q}{'}g;
201 106         202 $canonical_pattern =~ s{$Q}{''}g;
202              
203 106         198 $self->_positive_pattern($internal_pattern);
204 106         168 $self->_negative_pattern($M . $internal_pattern);
205              
206             # hashref instead of attribute method so wo don’t retrigger this trigger
207 106         2050 $self->{pattern} = $canonical_pattern;
208             }
209              
210             sub _validate_number {
211 320     320   364 my ($self, $method, $num) = @_;
212              
213 320 100       564 if (!defined $num) {
214 6         5 carp qq[Use of uninitialized value in ${\ref $self}::$method];
  6         69  
215 6         2466 return undef;
216             }
217              
218 314 100       685 if (!looks_like_number $num) {
219 14         20 carp qq[Argument "$num" isn't numeric in ${\ref $self}::$method];
  14         130  
220 16     16   20183 no warnings;
  16         26  
  16         17552  
221 14         5471 $num += 0;
222             }
223              
224 314         518 return $num;
225             }
226              
227             my $INF = 9**9**9;
228              
229             sub _format_number {
230 281     281   267 my ($self, $num) = @_;
231 281         209 my ($format, $num_format);
232              
233 281 100       474 if ($num < 0) {
234 30         62 my $pattern = $self->_negative_pattern;
235 30         167 $pattern =~ s{$M}{$self->minus_sign}e;
  29         84  
236 30         55 $format = $pattern;
237             }
238             else {
239 251         453 $format = $self->_positive_pattern;
240             }
241              
242 281 100 100     1594 if ($num == $INF || $num == -$INF) {
    100          
243 17         26 $num_format = $self->infinity;
244             }
245             elsif (!defined($num <=> $INF)) {
246 9         17 $num_format = $self->nan;
247             }
248             else {
249 255         200 my $rounded;
250              
251 255 100       561 if ($self->rounding_increment) {
252             # TODO: round half to even
253 23         559 $rounded = Math::Round::nearest(
254             $self->rounding_increment,
255             abs $num
256             );
257             }
258             else {
259             # round half to even
260 232         5722 $rounded = Math::BigFloat->new($num)->ffround(
261             -$self->maximum_fraction_digits,
262             'even'
263             )->babs->bstr;
264             }
265              
266 255         50168 my ($int, $frac) = split /\./, $rounded;
267 255 100       529 if (!defined $frac) {
268 50         58 $frac = '';
269             }
270              
271 255         566 my $primary_group = $self->primary_grouping_size;
272 255 100 100     6126 if (
273             $primary_group &&
274             $primary_group + $self->minimum_grouping_digits <= length $int
275             ) {
276 96         2067 my $group_sign = $self->group_sign;
277 96   66     245 my $other_groups = $self->secondary_grouping_size || $primary_group;
278              
279 96         2571 $int =~ s{ (?
280              
281 96         100 while (1) {
282 112 100       1495 last if $int !~ s{
283             (?
284             (?
285             (?= .{$other_groups} \Q$group_sign\E )
286             }{$group_sign}x;
287             }
288             }
289              
290 255   50     2945 my $int_pad = $self->minimum_integer_digits - (length $int || 0);
291 255 100       5255 if ($int_pad > 0) {
292 5         16 $int = 0 x $int_pad . $int;
293             }
294              
295 255   100     510 my $frac_pad = $self->minimum_fraction_digits - (length $frac || 0);
296 255 100       5125 if ($frac_pad > 0) {
    100          
297 2         6 $frac .= 0 x $frac_pad;
298             }
299             elsif ($frac_pad < 0) {
300 108         117 my $truncate_size = abs $frac_pad;
301 108         780 $frac =~ s{ 0{1,$truncate_size} $ }{}x;
302             }
303              
304 255         265 $num_format = $int;
305              
306 255 100       409 if (length $frac) {
307 162         340 $num_format .= $self->decimal_sign . $frac;
308             }
309              
310 255 100       578 if ($self->numbering_system ne 'latn') {
311             my $digits = $CLDR::Number::Data::System::DATA->{
312 9         164 $self->numbering_system
313             };
314              
315 9         197 $num_format =~ s{ ( [0-9] ) }{$digits->[$1]}xg;
316             }
317             }
318              
319 281         11398 $format =~ s{$N}{$num_format};
320              
321 281         1001 return $format;
322             }
323              
324             sub _process_num_pattern {
325 102     102   110 my ($self, $num_pattern) = @_;
326              
327 102         159 for ($num_pattern) {
328 102         145 s{ \. $ }{}x; # no trailing decimal sign
329 102         228 s{ (?: ^ | \# ) (?= \. ) }{0}x; # at least one minimum integer digit
330              
331             # calculate grouping sizes
332 102         215 my ($secondary, $primary) = map { length } m{
  62         86  
333             , ( [^,]* ) # primary
334             , ( [^,.]* ) # secondary
335             (?: \. | $ )
336             }x;
337              
338 102 100       220 if (!defined $primary) {
    100          
    100          
339 71         134 ($primary) = map { length } m{
  19         44  
340             , ( [^,.]* ) # primary only
341             (?: \. | $ )
342             }x;
343             }
344             elsif ($primary == 0) {
345 6         7 $primary = $secondary;
346 6         5 $secondary = undef;
347             }
348             elsif ($primary == $secondary) {
349 4         5 $secondary = undef;
350             }
351              
352 102         214 tr{,}{}d; # temporarily remove groups
353              
354 102 100       198 if (!m{ \. }x) {
355 80         163 s{ (?: ^ | \# ) $ }{0}x; # at least one minimum integer digit
356             }
357              
358 102 50       300 if (!$self->_has_init_arg('minimum_integer_digits')) {
359 102         276 my ($min_int) = m{ ( [0-9,]+ ) (?= \. | $ ) }x;
360 102         263 $self->minimum_integer_digits(length $min_int);
361             }
362              
363 102 100       656 if ($primary) {
364 42         460 s{ (?= .{$primary} (?: \. | $ ) ) }{,}x; # add primary group
365 42         120 $self->_set_unless_init_arg(primary_grouping_size => $primary);
366              
367 42 100       262 if ($secondary) {
368 19         111 s{ (?= .{$secondary} , ) }{,}x; # add secondary group
369 19         47 $self->_set_unless_init_arg(
370             secondary_grouping_size => $secondary
371             );
372             }
373             else {
374 23         55 $self->_set_unless_init_arg(secondary_grouping_size => 0);
375             }
376             }
377             else {
378 60         139 $self->_set_unless_init_arg(primary_grouping_size => 0);
379 60         392 $self->_set_unless_init_arg(secondary_grouping_size => 0);
380             }
381              
382 102         659 s{ ^ \#+ (?= [#0-9] ) }{}x; # no leading multiple #s
383 102         136 s{ ^ (?= , ) }{#}x; # leading # before group
384              
385 102 100       251 if (my ($max, $min) = m{ \. ( ( [0-9]* ) \#* ) }x) {
386 22         69 $self->_set_unless_init_arg(minimum_fraction_digits => length $min);
387 22         740 $self->_set_unless_init_arg(maximum_fraction_digits => length $max);
388             }
389             else {
390 80         154 $self->_set_unless_init_arg(minimum_fraction_digits => 0);
391 80         2551 $self->_set_unless_init_arg(maximum_fraction_digits => 0);
392             }
393              
394 102 50       3314 if (!$self->_has_init_arg('rounding_increment')) {
395 102 50       418 if (my ($round_inc) = m{ (
396             (?: [1-9] [0-9,]* | 0 ) # integer
397             (?= \. | $ )
398             (?: \. [0-9]* [1-9] )? # fraction
399             ) }x) {
400 102         182 $self->rounding_increment($round_inc);
401             }
402             else {
403 0         0 $self->rounding_increment(0);
404             }
405             }
406             }
407              
408 102         705 return $num_pattern;
409             }
410              
411             sub _escape_symbols {
412 102     102   101 my ($pattern) = @_;
413              
414 102         127 for ($pattern) {
415 102         130 s{%}{$P};
416 102         122 s{¤}{$C};
417 102         125 s{-}{$M};
418             }
419              
420 102         123 return $pattern;
421             }
422              
423             sub at_least {
424 6     6 0 134 my ($self, $num) = @_;
425 6         21 my $pattern = $self->_get_data(pattern => 'at_least');
426              
427 6         30 $num = $self->_validate_number(at_least => $num);
428 6 100       18 return undef unless defined $num;
429              
430 5         15 $num = $self->format($num);
431 5         15 $pattern =~ s{ \{ 0 \} }{$num}x;
432              
433 5         20 return $pattern;
434             }
435              
436             sub range {
437 15     15 0 243 my ($self, @nums) = @_;
438 15         44 my $pattern = $self->_get_data(pattern => 'range');
439              
440 15         22 for my $i (0, 1) {
441 29         64 my $num = $self->_validate_number(range => $nums[$i]);
442 29 100       55 return undef unless defined $num;
443              
444 27         75 $num = $self->format($num);
445 27         317 $pattern =~ s{ \{ $i \} }{$num}x;
446             }
447              
448 13         64 return $pattern;
449             }
450              
451             1;