File Coverage

blib/lib/Number/Bytes/Human.pm
Criterion Covered Total %
statement 159 177 89.8
branch 74 92 80.4
condition 47 67 70.1
subroutine 15 15 100.0
pod 6 6 100.0
total 301 357 84.3


line stmt bran cond sub pod time code
1             package Number::Bytes::Human;
2              
3 11     11   877381 use strict;
  11         22  
  11         306  
4 11     11   46 use warnings;
  11         14  
  11         786  
5              
6             our $VERSION = '0.11';
7              
8             require Exporter;
9             our @ISA = qw(Exporter);
10             our @EXPORT_OK = qw(format_bytes parse_bytes);
11              
12             require POSIX;
13 11     11   44 use Carp qw(croak carp);
  11         17  
  11         17608  
14              
15             #my $DEFAULT_BLOCK = 1024;
16             #my $DEFAULT_ZERO = '0';
17             #my $DEFAULT_ROUND_STYLE = 'ceil';
18             my %DEFAULT_SUFFIXES = (
19             1024 => ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'],
20             1000 => ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'],
21             1024000 => ['', 'M', 'T', 'E', 'Y'],
22             si_1024 => ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'],
23             si_1000 => ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
24             );
25             my @DEFAULT_PREFIXES = @{$DEFAULT_SUFFIXES{1024}};
26              
27             sub _default_suffixes {
28 129   50 129   219 my $set = shift || 1024;
29 129 50       225 if (exists $DEFAULT_SUFFIXES{$set}) {
30 129 50       205 return @{$DEFAULT_SUFFIXES{$set}} if wantarray;
  129         351  
31 0         0 return [ @{$DEFAULT_SUFFIXES{$set}} ];
  0         0  
32             }
33 0         0 croak "unknown suffix set '$set'";
34             }
35              
36             my %ROUND_FUNCTIONS = (
37             ceil => sub { return POSIX::ceil($_[0] * (10 ** $_[1])) / 10**$_[1]; },
38             floor => sub { return POSIX::floor($_[0] * (10 ** $_[1])) / 10**$_[1]; },
39             round => sub { return sprintf( "%." . ( $_[1] || 0 ) . "f", $_[0] ); },
40             trunc => sub { return sprintf( "%d", $_[0] * (10 ** $_[1])) / 10**$_[1]; },
41             # what about 'ceiling'?
42             );
43              
44             sub _round_function {
45 151     151   195 my $style = shift;
46 151 50       267 if (exists $ROUND_FUNCTIONS{$style}) {
47 151         266 return $ROUND_FUNCTIONS{$style}
48             }
49 0         0 croak "unknown round style '$style'";
50             }
51              
52             # options
53             # block | block_size | base | bs => 1024 | 1000
54             # base_1024 | block_1024 | 1024 => $true
55             # base_1000 | block_1000 | 1000 => $true
56             #
57             # round_function => \&
58             # round_style => 'ceiling', 'round', 'floor', 'trunc'
59             #
60             # suffixes => 1024 | 1000 | si_1024 | si_1000 | 1024000 | \@
61             # si => 1
62             # unit => string (eg., 'B' | 'bps' | 'b')
63             #
64             # zero => '0' (default) | '-' | '0%S' | undef
65             #
66             #
67             # supress_point_zero | no_point_zero =>
68             # b_to_i => 1
69             # to_s => \&
70             #
71             # allow_minus => 0 | 1
72             # too_large => string
73             # quiet => 1 (supresses "too large number" warning)
74              
75              
76              
77             # PROBABLY CRAP:
78             # precision => integer
79              
80             # parsed options
81             # BLOCK => 1024 | 1000
82             # ROUND_STYLE => 'ceil', 'round', 'floor', 'trunc'
83             # ROUND_FUNCTION => \&
84             # SUFFIXES => \@
85             # ZERO =>
86             # SI => undef | 1 Parse SI compatible
87              
88              
89             =begin private
90              
91             $options = _parse_args($seed, $args)
92             $options = _parse_args($seed, arg1 => $val1, ...)
93              
94             $seed is undef or a hashref
95             $args is a hashref
96              
97             =end private
98              
99             =cut
100              
101             sub _parse_args {
102 177     177   4051 my $seed = shift;
103 177         182 my %args;
104              
105             my %options;
106 177 100       352 unless (defined $seed) { # use defaults
107 149         193 $options{BLOCK} = 1024;
108 149         162 $options{ROUND_STYLE} = 'ceil';
109 149         195 $options{ROUND_FUNCTION} = _round_function($options{ROUND_STYLE});
110 149         149 $options{ZERO} = '0';
111 149         126 $options{SI} = undef;
112 149         127 $options{PRECISION} = 1;
113 149         123 $options{PRECISION_CUTOFF} = 1;
114             #$options{SUFFIXES} = # deferred to the last minute when we know BLOCK, seek [**]
115 149         282 $options{UNIT} = undef;
116             }
117             # else { %options = %$seed } # this is set if @_!=0, down below
118              
119 177 100 66     515 if (@_==0) { # quick return for default values (no customized args)
    100          
120 67 50       178 return (defined $seed) ? $seed : \%options;
121             } elsif (@_==1 && ref $_[0]) { # \%args
122 27         19 %args = %{$_[0]};
  27         78  
123             } else { # arg1 => $val1, arg2 => $val2
124 83         171 %args = @_;
125             }
126              
127             # this is done here so this assignment/copy doesn't happen if @_==0
128 110 100       198 %options = %$seed unless %options;
129              
130             # block | block_size | base | bs => 1024 | 1000
131             # block_1024 | base_1024 | 1024 => $true
132             # block_1000 | base_1000 | 1024 => $true
133 110 100 66     701 if ($args{block} ||
    100 66        
    100 66        
      66        
      66        
      66        
      66        
134             $args{block_size} ||
135             $args{base} ||
136             $args{bs}
137             ) {
138             my $block = $args{block} ||
139             $args{block_size} ||
140             $args{base} ||
141 76   66     277 $args{bs};
142 76 100 100     236 unless ($block==1000 || $block==1024 || $block==1_024_000) {
      100        
143 1         175 croak "invalid base: $block (should be 1024, 1000 or 1024000)";
144             }
145 75         88 $options{BLOCK} = $block;
146              
147             } elsif ($args{block_1024} ||
148             $args{base_1024} ||
149             $args{1024}) {
150              
151 3         5 $options{BLOCK} = 1024;
152             } elsif ($args{block_1000} ||
153             $args{base_1000} ||
154             $args{1000}) {
155              
156 3         5 $options{BLOCK} = 1000;
157             }
158              
159             # round_function => \&
160             # round_style => 'ceil' | 'floor' | 'round' | 'trunc'
161 109 100       228 if ($args{round_function}) {
    100          
162 4 100       10 unless (ref $args{round_function} eq 'CODE') {
163 2         204 croak "round function ($args{round_function}) should be a code ref";
164             }
165 2         3 $options{ROUND_FUNCTION} = $args{round_function};
166 2   100     7 $options{ROUND_STYLE} = $args{round_style} || 'unknown';
167             } elsif ($args{round_style}) {
168 2         4 $options{ROUND_FUNCTION} = _round_function($args{round_style});
169 2         3 $options{ROUND_STYLE} = $args{round_style};
170             }
171              
172             # SI compatibility (mostly for parsing)
173 107 100       159 if ($args{si}) {
174 15         18 $options{SI} = 1;
175             }
176              
177             # suffixes => 1024 | 1000 | si_1024 | si_1000 | 1024000 | \@
178 107 100       139 if ($args{suffixes}) {
179 4 50       12 if (ref $args{suffixes} eq 'ARRAY') {
    0          
180 4         7 $options{SUFFIXES} = $args{suffixes};
181             } elsif ($args{suffixes} =~ /^(si_)?(1000|1024)$/) {
182 0         0 $options{SUFFIXES} = _default_suffixes($args{suffixes});
183             } else {
184 0         0 croak "suffixes ($args{suffixes}) should be 1024, 1000, si_1024, si_1000, 1024000 or an array ref";
185             }
186             }
187 107 100       154 if (defined $args{unit}) {
188 6         7 $options{UNIT} = $args{unit};
189             }
190              
191             # zero => undef | string
192 107 100       154 if (exists $args{zero}) {
193 11         14 $options{ZERO} = $args{zero};
194 11 100       25 if (defined $options{ZERO}) {
195 9         23 $options{ZERO} =~ s/%S/$options{SUFFIXES}->[0]/g
196             }
197             }
198              
199             # precision =>
200 107 100 66     198 if (exists $args{precision} and $args{precision} =~ /\A\d+\z/) {
201 2         5 $options{PRECISION} = $args{precision};
202             }
203              
204             # precision_cutoff =>
205 107 50 50     176 if (exists $args{precision_cutoff} and ($args{precision_cutoff} =~ /\A\d+\z/ or $args{precision_cutoff} = '-1')) {
      66        
206 1         2 $options{PRECISION_CUTOFF} = $args{precision_cutoff};
207             }
208              
209             # quiet => 1
210 107 50       140 if ($args{quiet}) {
211 0         0 $options{QUIET} = 1;
212             }
213              
214 107 100       150 if (defined $seed) {
215 25         46 %$seed = %options;
216 25         121 return $seed;
217             }
218 82         176 return \%options
219             }
220              
221             # NOTE. _format_bytes() SHOULD not change $options - NEVER.
222              
223             sub _format_bytes {
224 73     73   59 my $bytes = shift;
225 73 100       127 return undef unless defined $bytes;
226 70         53 my $options = shift;
227 70         257 my %options = %$options;
228              
229 70         166 local *human_round = $options{ROUND_FUNCTION};
230              
231 70 100 100     248 return $options{ZERO} if ($bytes==0 && defined $options{ZERO});
232              
233 59         54 my $block = $options{BLOCK};
234              
235             # if a suffix set was not specified, pick a default [**]
236 59 100       225 my @suffixes = $options{SUFFIXES} ? @{$options{SUFFIXES}} : _default_suffixes( ($options{SI} ? 'si_' : '') . $block);
  1 100       2  
237              
238             # WHAT ABOUT NEGATIVE NUMBERS: -1K ?
239 59         77 my $sign = '';
240 59 100       94 if ($bytes<0) {
241 3         7 $bytes = -$bytes;
242 3         5 $sign = '-';
243             }
244              
245 59         48 my $suffix = $suffixes[0];
246 59         51 my $x = $bytes;
247 59         44 my $magnitude = 0;
248 59 100       95 if($bytes >= $block) {
249             # return "$sign$bytes" if $bytes<$block;
250             do {
251 67         55 $x /= $block;
252 67         101 $magnitude++;
253 30         26 } while ( human_round($x, $options{PRECISION}) >= $block );
254 30 50       65 if($magnitude >= (0 + @suffixes)) {
255 0 0       0 carp "number too large (>= $block**$magnitude)" unless ($options{QUIET});
256             }
257 30         39 $suffix = $suffixes[$magnitude];
258             }
259             #$x = human_round( $x, $options{PRECISION} );
260              
261 59         77 $x = _precision_cutoff($x, $options);
262             #reasses encase the precision_cutoff caused the value to cross the block size
263 59 100       142 if($x >= $block) {
264 1         2 $x /= $block;
265 1         2 $magnitude++;
266 1 50       4 if($magnitude >= (0 + @suffixes)) {
267 0 0       0 carp "number too large (>= $block**$magnitude)" unless ($options{QUIET});
268             }
269 1         2 $suffix = $suffixes[$magnitude];
270 1         2 $x = _precision_cutoff($x, $options);
271             }
272              
273 59   100     167 my $unit = $options{UNIT} || '';
274              
275 59         451 return $sign . $x . $suffix . $unit;
276              
277             }
278              
279             sub _precision_cutoff {
280 60     60   53 my $bytes = shift;
281 60         49 my $options = shift;
282 60         168 my %options = %$options;
283 60 100 66     389 if ( $options{PRECISION_CUTOFF} != -1 and ( length( sprintf( "%d", $bytes ) ) > $options{PRECISION_CUTOFF} ) ) {
284 33         61 $bytes = sprintf( "%d", human_round( $bytes, 0 ) );
285             } else {
286 27         93 $bytes = sprintf( "%." . $options{PRECISION} . "f", human_round( $bytes, $options{PRECISION} ) );
287             }
288 60         158 return $bytes;
289             }
290              
291             sub _parse_bytes {
292 93     93   77 my $human = shift;
293 93         95 my $options = shift;
294 93         354 my %options = %$options;
295              
296 93 100 66     741 return 0 if( exists $options{ZERO} && ((!defined $options{ZERO} && !defined $human) || (defined $human && $human eq $options{ZERO})) );
      33        
297 80 100       135 return undef unless defined $human;
298              
299 77         64 my %suffix_mult;
300             my %suffix_block;
301 0         0 my $m;
302              
303 77 50       110 if( $options{SUFFIXES} ) {
304 0         0 $m = 1;
305 0         0 foreach my $s (@{$options{SUFFIXES}}) {
  0         0  
306 0         0 $suffix_mult{$s} = $m;
307 0         0 $suffix_block{$s} = $options{BLOCK};
308 0         0 $m *= $suffix_block{$s};
309             }
310             } else {
311 77 50 66     170 if( !defined $options{SI} || $options{SI} == 1 ) {
312             # If SI compatibility has been set BLOCK is ignored as it is infered from the unit
313 77         58 $m = 1;
314 77         48 foreach my $s (@{$DEFAULT_SUFFIXES{si_1000}}) {
  77         147  
315 693         614 $suffix_mult{$s} = $m;
316 693         520 $suffix_block{$s} = 1000;
317 693         777 $m *= $suffix_block{$s};
318             }
319            
320 77         71 $m = 1;
321 77         77 foreach my $s (@{$DEFAULT_SUFFIXES{si_1024}}) {
  77         93  
322 693         592 $suffix_mult{$s} = $m;
323 693         526 $suffix_block{$s} = 1024;
324 693         640 $m *= $suffix_block{$s};
325             }
326             }
327              
328             # The regular suffixes are only taken into account in default mode without specifically asking for SI compliance
329 77 100       166 if( !defined $options{SI} ) {
330 71         60 $m = 1;
331 71         114 foreach my $s (_default_suffixes( $options{BLOCK} )) {
332 595         557 $suffix_mult{$s} = $m;
333 595         476 $suffix_block{$s} = $options{BLOCK};
334 595         544 $m *= $suffix_block{$s};
335             }
336             }
337             }
338              
339 77         448 my ($sign, $k, $unit) = ($human =~ /^\s*(-?)\s*(\d*(?:\.\d*)?)\s*(\D*)$/);
340              
341             # print STDERR "S: $sign K: $k U: $unit\n";
342              
343              
344 77         70 my $mult;
345 77   100     225 my $u = $options{UNIT} || '';
346 77         237 foreach my $s (keys %suffix_block) {
347 1065 100       5139 if( $unit =~ /^${s}${u}$/i ) {
348 77 100       147 $mult = ($sign eq '-' ? -1 : 1) * $suffix_mult{$s};
349 77         106 last;
350             }
351             }
352              
353 77 50       180 if( !defined $mult ) {
354 0         0 carp "Could not parse human readable byte value '$human'";
355 0         0 return undef;
356             }
357              
358 77         165 my $bytes = int( $k * $mult );
359              
360 77         855 return $bytes;
361             }
362              
363              
364             # convert byte count (file size) to human readable format
365             sub format_bytes {
366 71     71 1 26276 my $bytes = shift;
367 71         132 my $options = _parse_args(undef, @_);
368             #use YAML; print Dump $options;
369 71         108 return _format_bytes($bytes, $options);
370             }
371              
372             # convert human readable format to byte count (file size)
373             sub parse_bytes {
374 76     76 1 15431 my $human = shift;
375 76         136 my $options = _parse_args(undef, @_);
376             #use YAML; print Dump $options;
377 76         116 return _parse_bytes($human, $options);
378             }
379              
380             ### the OO way
381              
382             # new()
383             sub new {
384 2     2 1 660 my $proto = shift;
385 2   33     12 my $class = ref $proto || $proto;
386 2         5 my $opts = _parse_args(undef, @_);
387 2         6 return bless $opts, $class;
388             }
389              
390             # set_options()
391             sub set_options {
392 1     1 1 2 my $self = shift;
393 1         2 return $self->_parse_args(@_);
394             }
395              
396             # format()
397             sub format {
398 2     2 1 383 my $self = shift;
399 2         5 my $bytes = shift;
400 2         5 return _format_bytes($bytes, $self);
401             }
402              
403             # parse()
404             sub parse {
405 17     17 1 27 my $self = shift;
406 17         17 my $human = shift;
407 17         23 return _parse_bytes($human, $self);
408             }
409              
410             # the solution by COG in Filesys::DiskUsage
411             # convert size to human readable format
412             #sub _convert {
413             # defined (my $size = shift) || return undef;
414             # my $config = {@_};
415             # $config->{human} || return $size;
416             # my $block = $config->{'Human-readable'} ? 1000 : 1024;
417             # my @args = qw/B K M G/;
418             #
419             # while (@args && $size > $block) {
420             # shift @args;
421             # $size /= $block;
422             # }
423             #
424             # if ($config->{'truncate-readable'} > 0) {
425             # $size = sprintf("%.$config->{'truncate-readable'}f",$size);
426             # }
427             #
428             # "$size$args[0]";
429             #}
430             #
431             # not exact: 1024 => 1024B instead of 1K
432             # not nicely formatted => 1.00 instead of 1K
433              
434             1;
435              
436             __END__