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   20261 use v5.8.1;
  16         51  
4 16     16   113 use utf8;
  16         52  
  16         84  
5 16     16   575 use Carp;
  16         29  
  16         918  
6 16     16   76 use Scalar::Util qw( looks_like_number );
  16         31  
  16         765  
7 16     16   22197 use Math::BigFloat;
  16         334004  
  16         102  
8 16     16   615391 use Math::Round;
  16         143902  
  16         1199  
9 16     16   130 use CLDR::Number::Constant qw( $N $M $P $C $Q );
  16         34  
  16         2375  
10 16     16   874 use CLDR::Number::Data::Base;
  16         41  
  16         432  
11 16     16   749 use CLDR::Number::Data::System;
  16         31  
  16         347  
12              
13 16     16   82 use Moo::Role;
  16         35  
  16         188  
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.16';
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   206 my ($self) = @_;
125              
126 138         591 $self->_set_unless_init_arg(
127             pattern => $self->_get_data(pattern => $self->_pattern_type)
128             );
129             }
130              
131             sub _trigger_pattern {
132 246     246   3133 my ($self, $input_pattern) = @_;
133              
134 246         377 my $cache = $CLDR::Number::Data::Base::CACHE;
135 246 100 66     2022 if (my $attributes
136             = $cache->{attribute}{$input_pattern}
137             || $cache->{pattern}{$input_pattern}
138             && $cache->{attribute}{ $cache->{pattern}{$input_pattern}[0] }) {
139              
140 140         637 while (my ($attribute, $value) = each %$attributes) {
141 840         24949 $self->_set_unless_init_arg($attribute => $value);
142             }
143              
144 140         2515 my $pattern = $cache->{pattern}{$input_pattern};
145              
146 140   66     925 $self->_positive_pattern(
147             $pattern && $pattern->[1] || $N
148             );
149              
150 140   66     1093 $self->_negative_pattern(
151             $pattern && $pattern->[2] || $M . $self->_positive_pattern
152             );
153              
154 140         2915 return;
155             }
156              
157             # temporarily replace escaped quotes
158 106         197 $input_pattern =~ s{''}{$Q}g;
159              
160 106         149 my $internal_pattern = '';
161 106         142 my $canonical_pattern = '';
162 106         129 my $num_subpattern;
163              
164 106         473 while ($input_pattern =~ m{
165             \G (?:
166             ( [^']+ ) # non-quoted text
167             |
168             ' ( [^']+ ) (?: ' | $ ) # quoted text (trailing quote optional)
169             )
170             }xg) {
171 111         307 my $nonquoted = $1;
172 111         157 my $quoted = $2;
173              
174 111 100       247 if (defined $nonquoted) {
    50          
175 102 50 33     6660 if (!defined $num_subpattern && $nonquoted =~ m{
176             ^ ( .*? ) # pre–number pattern
177             ( (?: \* \X )? [@#0-9,.]+ ) # number pattern
178             ( .* ) $ # post–number pattern
179             }x) {
180 102         181 my $prenum = $1;
181 102         149 $num_subpattern = $2;
182 102         159 my $postnum = $3;
183              
184 102         264 $num_subpattern = $self->_process_num_pattern($num_subpattern);
185              
186 102         383 $internal_pattern .= _escape_symbols($prenum . $N . $postnum);
187 102         427 $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         16 $internal_pattern .= $quoted;
196 9         35 $canonical_pattern .= "'$quoted'";
197             }
198             }
199              
200 106         361 $internal_pattern =~ s{$Q}{'}g;
201 106         266 $canonical_pattern =~ s{$Q}{''}g;
202              
203 106         296 $self->_positive_pattern($internal_pattern);
204 106         344 $self->_negative_pattern($M . $internal_pattern);
205              
206             # hashref instead of attribute method so wo don’t retrigger this trigger
207 106         2179 $self->{pattern} = $canonical_pattern;
208             }
209              
210             sub _validate_number {
211 320     320   534 my ($self, $method, $num) = @_;
212              
213 320 100       732 if (!defined $num) {
214 6         8 carp qq[Use of uninitialized value in ${\ref $self}::$method];
  6         89  
215 6         4044 return undef;
216             }
217              
218 314 100       971 if (!looks_like_number $num) {
219 14         29 carp qq[Argument "$num" isn't numeric in ${\ref $self}::$method];
  14         158  
220 16     16   27202 no warnings;
  16         52  
  16         24162  
221 14         8845 $num += 0;
222             }
223              
224 314         743 return $num;
225             }
226              
227             my $INF = 9**9**9;
228              
229             sub _format_number {
230 281     281   436 my ($self, $num) = @_;
231 281         335 my ($format, $num_format);
232              
233 281 100       600 if ($num < 0) {
234 30         80 my $pattern = $self->_negative_pattern;
235 30         199 $pattern =~ s{$M}{$self->minus_sign}e;
  29         104  
236 30         64 $format = $pattern;
237             }
238             else {
239 251         604 $format = $self->_positive_pattern;
240             }
241              
242 281 100 100     1576 if ($num == $INF || $num == -$INF) {
    100          
243 17         40 $num_format = $self->infinity;
244             }
245             elsif (!defined($num <=> $INF)) {
246 9         22 $num_format = $self->nan;
247             }
248             else {
249 255         273 my $rounded;
250              
251 255 100       781 if ($self->rounding_increment) {
252             # TODO: round half to even
253 23         635 $rounded = Math::Round::nearest(
254             $self->rounding_increment,
255             abs $num
256             );
257             }
258             else {
259             # round half to even
260 232         6939 $rounded = Math::BigFloat->new($num)->ffround(
261             -$self->maximum_fraction_digits,
262             'even'
263             )->babs->bstr;
264             }
265              
266 255         66308 my ($int, $frac) = split /\./, $rounded;
267 255 100       667 if (!defined $frac) {
268 50         82 $frac = '';
269             }
270              
271 255         740 my $primary_group = $self->primary_grouping_size;
272 255 100 100     7177 if (
273             $primary_group &&
274             $primary_group + $self->minimum_grouping_digits <= length $int
275             ) {
276 96         2625 my $group_sign = $self->group_sign;
277 96   66     366 my $other_groups = $self->secondary_grouping_size || $primary_group;
278              
279 96         3208 $int =~ s{ (?
280              
281 96         147 while (1) {
282 112 100       2039 last if $int !~ s{
283             (?
284             (?
285             (?= .{$other_groups} \Q$group_sign\E )
286             }{$group_sign}x;
287             }
288             }
289              
290 255   50     3962 my $int_pad = $self->minimum_integer_digits - (length $int || 0);
291 255 100       6716 if ($int_pad > 0) {
292 5         26 $int = 0 x $int_pad . $int;
293             }
294              
295 255   100     715 my $frac_pad = $self->minimum_fraction_digits - (length $frac || 0);
296 255 100       7129 if ($frac_pad > 0) {
    100          
297 2         8 $frac .= 0 x $frac_pad;
298             }
299             elsif ($frac_pad < 0) {
300 108         153 my $truncate_size = abs $frac_pad;
301 108         908 $frac =~ s{ 0{1,$truncate_size} $ }{}x;
302             }
303              
304 255         429 $num_format = $int;
305              
306 255 100       596 if (length $frac) {
307 162         552 $num_format .= $self->decimal_sign . $frac;
308             }
309              
310 255 100       727 if ($self->numbering_system ne 'latn') {
311             my $digits = $CLDR::Number::Data::System::DATA->{
312 9         241 $self->numbering_system
313             };
314              
315 9         290 $num_format =~ s{ ( [0-9] ) }{$digits->[$1]}xg;
316             }
317             }
318              
319 281         15095 $format =~ s{$N}{$num_format};
320              
321 281         1366 return $format;
322             }
323              
324             sub _process_num_pattern {
325 102     102   181 my ($self, $num_pattern) = @_;
326              
327 102         228 for ($num_pattern) {
328 102         186 s{ \. $ }{}x; # no trailing decimal sign
329 102         305 s{ (?: ^ | \# ) (?= \. ) }{0}x; # at least one minimum integer digit
330              
331             # calculate grouping sizes
332 102         297 my ($secondary, $primary) = map { length } m{
  62         118  
333             , ( [^,]* ) # primary
334             , ( [^,.]* ) # secondary
335             (?: \. | $ )
336             }x;
337              
338 102 100       294 if (!defined $primary) {
    100          
    100          
339 71         184 ($primary) = map { length } m{
  19         60  
340             , ( [^,.]* ) # primary only
341             (?: \. | $ )
342             }x;
343             }
344             elsif ($primary == 0) {
345 6         9 $primary = $secondary;
346 6         10 $secondary = undef;
347             }
348             elsif ($primary == $secondary) {
349 4         10 $secondary = undef;
350             }
351              
352 102         304 tr{,}{}d; # temporarily remove groups
353              
354 102 100       289 if (!m{ \. }x) {
355 80         216 s{ (?: ^ | \# ) $ }{0}x; # at least one minimum integer digit
356             }
357              
358 102 50       395 if (!$self->_has_init_arg('minimum_integer_digits')) {
359 102         376 my ($min_int) = m{ ( [0-9,]+ ) (?= \. | $ ) }x;
360 102         342 $self->minimum_integer_digits(length $min_int);
361             }
362              
363 102 100       912 if ($primary) {
364 42         531 s{ (?= .{$primary} (?: \. | $ ) ) }{,}x; # add primary group
365 42         156 $self->_set_unless_init_arg(primary_grouping_size => $primary);
366              
367 42 100       359 if ($secondary) {
368 19         122 s{ (?= .{$secondary} , ) }{,}x; # add secondary group
369 19         66 $self->_set_unless_init_arg(
370             secondary_grouping_size => $secondary
371             );
372             }
373             else {
374 23         66 $self->_set_unless_init_arg(secondary_grouping_size => 0);
375             }
376             }
377             else {
378 60         196 $self->_set_unless_init_arg(primary_grouping_size => 0);
379 60         615 $self->_set_unless_init_arg(secondary_grouping_size => 0);
380             }
381              
382 102         895 s{ ^ \#+ (?= [#0-9] ) }{}x; # no leading multiple #s
383 102         223 s{ ^ (?= , ) }{#}x; # leading # before group
384              
385 102 100       346 if (my ($max, $min) = m{ \. ( ( [0-9]* ) \#* ) }x) {
386 22         83 $self->_set_unless_init_arg(minimum_fraction_digits => length $min);
387 22         984 $self->_set_unless_init_arg(maximum_fraction_digits => length $max);
388             }
389             else {
390 80         220 $self->_set_unless_init_arg(minimum_fraction_digits => 0);
391 80         3408 $self->_set_unless_init_arg(maximum_fraction_digits => 0);
392             }
393              
394 102 50       4441 if (!$self->_has_init_arg('rounding_increment')) {
395 102 50       591 if (my ($round_inc) = m{ (
396             (?: [1-9] [0-9,]* | 0 ) # integer
397             (?= \. | $ )
398             (?: \. [0-9]* [1-9] )? # fraction
399             ) }x) {
400 102         301 $self->rounding_increment($round_inc);
401             }
402             else {
403 0         0 $self->rounding_increment(0);
404             }
405             }
406             }
407              
408 102         983 return $num_pattern;
409             }
410              
411             sub _escape_symbols {
412 102     102   144 my ($pattern) = @_;
413              
414 102         192 for ($pattern) {
415 102         178 s{%}{$P};
416 102         172 s{¤}{$C};
417 102         199 s{-}{$M};
418             }
419              
420 102         207 return $pattern;
421             }
422              
423             sub at_least {
424 6     6 0 158 my ($self, $num) = @_;
425 6         24 my $pattern = $self->_get_data(pattern => 'at_least');
426              
427 6         43 $num = $self->_validate_number(at_least => $num);
428 6 100       25 return undef unless defined $num;
429              
430 5         20 $num = $self->format($num);
431 5         22 $pattern =~ s{ \{ 0 \} }{$num}x;
432              
433 5         25 return $pattern;
434             }
435              
436             sub range {
437 15     15 0 316 my ($self, @nums) = @_;
438 15         54 my $pattern = $self->_get_data(pattern => 'range');
439              
440 15         31 for my $i (0, 1) {
441 29         81 my $num = $self->_validate_number(range => $nums[$i]);
442 29 100       69 return undef unless defined $num;
443              
444 27         90 $num = $self->format($num);
445 27         355 $pattern =~ s{ \{ $i \} }{$num}x;
446             }
447              
448 13         71 return $pattern;
449             }
450              
451             1;