File Coverage

blib/lib/Number/Textify.pm
Criterion Covered Total %
statement 56 58 96.5
branch 38 42 90.4
condition 16 23 69.5
subroutine 8 8 100.0
pod 2 2 100.0
total 120 133 90.2


line stmt bran cond sub pod time code
1             package Number::Textify;
2             $Number::Textify::VERSION = '20200511';
3              
4 3     3   568511 use strict;
  3         17  
  3         89  
5 3     3   652 use utf8;
  3         20  
  3         15  
6 3     3   75 use warnings;
  3         6  
  3         2436  
7              
8             =encoding utf8
9              
10             =head1 NAME
11              
12             Number::Textify - turn number into some string.
13              
14             =head1 VERSION
15              
16             version 20200511
17              
18             =head1 SYNOPSIS
19              
20             use Number::Textify ();
21              
22             my $time_converter = Number::Textify
23             -> new( [ [ 60 * 60, 'hour' ],
24             [ 60, 'minute' ],
25             [ 0, 'second' ],
26             ],
27              
28             skip_zeroes => 1,
29             );
30              
31             print $time_converter -> textify( 7274 ); # 2 hours 1 minute 14 seconds
32              
33              
34             my $time_converter_digital_neat = Number::Textify
35             -> new( [ [ 24 * 60 * 60, '%dd ' ],
36             [ 60 * 60, '%02d:' ],
37             [ 60, '%02d:' ],
38             [ 0, '%02d' ],
39             ],
40              
41             joiner => '',
42             formatter => sub { my $format = $_[ 1 ] // '%02d';
43             sprintf $format,
44             $_[ 0 ];
45             },
46             post_process => sub {
47             $_[ 0 ] =~ s/^0+//;
48             $_[ 0 ];
49             },
50             );
51              
52             print $time_converter_digital_neat -> textify( 10_000_000 ); # 115d 17:46:40
53              
54              
55             my $size_converter = Number::Textify
56             -> new( [ [ 1_024 * 1_024 * 1_024, '%.2f GiB' ],
57             [ 1_024 * 1_024, '%.2f MiB' ],
58             [ 1_024, '%.2f KiB' ],
59             [ 0, '%d B' ],
60             ],
61              
62             rounding => 1,
63             formatter => sub { sprintf $_[ 1 ],
64             $_[ 0 ];
65             },
66             );
67              
68             print $size_converter -> textify( 10_000_000 ); # 9.54 MiB
69              
70             =head1 DESCRIPTION
71              
72             Number::Textify helps you to build and use converter of number to some text representation of it.
73              
74             For example 10_000_000 can be represented as '115 days 17 hours 46 minutes 40 seconds', '115.7 days', '115d 17:46:40', '9.54 MiB' or '10.00 MB'. You can see some examples in t/02-examples.t
75              
76             This module uses object oriented approach and doesn't export anything.
77              
78             =head1 OBJECT CREATION
79              
80             =head2 new
81              
82             Expects one required parameter and a hash of optional ones. If some incorrectness detected, it dies.
83              
84             First (and only required) parameter is an arrayref to arrayrefs. First element in the nested arrayref is a positive number, which is a threshold for the range. The rest of elements are passed to the formatter. Arrayrefs should be sorted by the first element in descending order.
85              
86             Range representation figured using the threshold is passed to the formatter along with the rest of elements in the nested arrayref.
87              
88             Default formatter joins the range representation with the first of the rest of elements in the arrayref. Unless range representation equals 1, adds 's' to the result.
89              
90             If you need something else instead of that, you can pass a pair:
91              
92             formatter => sub { my ( $range_representation, @tail_of_nested_arrayref ) = @_; .. },
93              
94             Then those formatted range representations are joined with the default joiner, which is ' ' (a space). If you want to use another joiner, you can provide it as:
95              
96             joiner => ':',
97              
98             Then the joined result is passed through the post_process sub, which by default doesn't change anything. If you want to do some processing though, you can replace it:
99              
100             post_process => sub { my $result = shift; .. },
101              
102             If you prefer to avoid zero values in the middle of result ( like '2 hours 0 minutes 14 seconds' ), you can use the option:
103              
104             skip_zeroes => 1,
105              
106             If you don't want the exact representation, but only some rounding, there's an option for that:
107              
108             rounding => 1,
109              
110             though in this case it usually has sense to provide a custom formatter too.
111              
112             =cut
113              
114             sub new {
115 14 50   14 1 9436 ref( my $class = shift )
116             and die "I'm only a class method!";
117              
118 14 100       49 die 'Constructor expects at least one argument'
119             unless @_ > 0;
120              
121 13         17 my $ranges = shift;
122              
123 13 100       39 die 'Incorrect number of additional parameters (hash expected)'
124             if @_ % 2;
125              
126 12         32 my %arg = @_;
127              
128             # let's check that ranges are in the way we expect (as structure)
129 12 100 33     256 die 'Ranges should be defined as array of arrays'
      66        
      66        
      100        
130             unless $ranges
131             && 'ARRAY' eq ref $ranges
132             && @$ranges
133             && ! grep( ! ( $_
134             && 'ARRAY' eq ref $_
135             && @$_ >= 1 # range cutoff, additional parameters (e. g. description string) for formatter
136             && $_ -> [ 0 ] >= 0 # range cutoff should be positive
137             ),
138             @$ranges
139             )
140             ;
141              
142             # now let's check that ranges are in descending order
143 9         21 my $prev_range = $ranges -> [ 0 ][ 0 ];
144 9         47 for my $range
145             ( @$ranges[ 1 .. $#$ranges ]
146             ) {
147 23 100       60 die 'Ranges should be defined in descending order'
148             unless $prev_range > $range -> [ 0 ];
149              
150 21         31 $prev_range = $range -> [ 0 ];
151             }
152              
153             my %self =
154             ( ranges => $ranges,
155             joiner => exists $arg{joiner} ? $arg{joiner} : ' ',
156             skip_zeroes => exists $arg{skip_zeroes} ? !! $arg{skip_zeroes} : '',
157             rounding => exists $arg{rounding} ? !! $arg{rounding} : '',
158              
159             map {
160 7 100       32 exists $arg{ $_ }
    100          
    100          
161             && $arg{ $_ }
162             && 'CODE' eq ref $arg{ $_ } ?
163 14 100 33     77 ( $_ => $arg{ $_ } )
164             : ();
165             }
166             qw/ formatter
167             post_process
168             /,
169             );
170              
171 7         27 bless \ %self, $class;
172             }
173              
174             =head1 OBJECT METHODS
175              
176             =head2 textify
177              
178             Returns text presentation of the only one passed numeric parameter.
179              
180             =cut
181              
182             sub textify {
183 53 50   53 1 4416 ref( my $self = shift )
184             or die "I'm only an object method!";
185              
186 53         83 my $value = shift;
187 53         69 my $sign = '';
188 53 50       107 if ( $value < 0
189             ) {
190 0         0 $sign = '-';
191 0         0 $value *= -1;
192             }
193              
194 53         74 my @result;
195              
196 53         78 for my $range
197 53         110 ( @{ $self -> {ranges} }
198             ) {
199 170         206 my $t;
200 170         308 ( $t, $value ) = $self -> _range_value( $value, $range -> [ 0 ] );
201 170 100 100     448 if ( $t
      100        
202             || ( @result
203             && ! $self -> {skip_zeroes}
204             )
205             ) {
206 129         325 push @result,
207             $self -> _formatter( $t, @$range[ 1 .. $#$range ] );
208             }
209              
210             last
211 170 100       770 unless defined $value;
212             }
213              
214             $self
215             -> _post_process( ( $sign ? $sign : '' )
216             . join $self -> {joiner},
217             @result
218 53 50       205 );
219             }
220              
221             sub _range_value { # returns numeric value for current range, value for the next range (undef if not needed to process further)
222 170     170   225 my $self = shift;
223 170         230 my $value = shift;
224 170         232 my $range = shift;
225              
226 170 100       275 if ( $value >= $range
227             ) { # there's something in this range
228 117 100       226 my $value_for_range = $range ?
229             $value / $range
230             : $value;
231              
232 117 100       206 if ( $self -> {rounding}
233             ) { # range value is rounded. no need to continue
234 17         41 ( $value_for_range, undef );
235             } else { # range value is whole
236 100         144 $value_for_range = int( $value_for_range );
237              
238 100 100       188 my $for_next_range = $range ?
239             $value % $range
240             : undef;
241              
242 100         236 ( $value_for_range, $for_next_range );
243             }
244             } else { # pass to the next range
245 53         114 ( 0, $value );
246             }
247             }
248              
249             sub _formatter {
250 129     129   179 my $self = shift;
251              
252 129 100       223 if ( $self -> {formatter}
253             ) {
254 75         155 $self -> {formatter} -> ( @_ );
255             } else {
256 54         74 my $value = shift;
257 54         74 my $string = shift;
258              
259 54 100 50     232 sprintf '%s %s%s',
260             $value,
261             $string // '',
262             $value == 1 ? '' : 's',
263             ;
264             }
265             }
266              
267             sub _post_process {
268 53     53   88 my $self = shift;
269              
270 53 100       94 if ( $self -> {post_process}
271             ) {
272 9         21 $self -> {post_process} -> ( @_ );
273             } else {
274 44         275 $_[ 0 ];
275             }
276             }
277              
278             =head1 AUTHOR
279              
280             Valery Kalesnik, C<< >>
281              
282             =head1 COPYRIGHT AND LICENSE
283              
284             This software is copyright (c) 2020 by Valery Kalesnik.
285              
286             This is free software; you can redistribute it and/or modify it under
287             the same terms as the Perl 5 programming language system itself.
288              
289             =cut
290              
291             1;