File Coverage

blib/lib/Number/Bytes/Human.pm
Criterion Covered Total %
statement 164 183 89.6
branch 74 92 80.4
condition 56 69 81.1
subroutine 16 16 100.0
pod 6 6 100.0
total 316 366 86.3


line stmt bran cond sub pod time code
1             package Number::Bytes::Human;
2              
3 10     10   22301 use strict;
  10         24  
  10         465  
4 10     10   62 use warnings;
  10         20  
  10         1050  
5              
6             our $VERSION = '0.09';
7              
8             require Exporter;
9             our @ISA = qw(Exporter);
10             our @EXPORT_OK = qw(format_bytes parse_bytes);
11              
12             require POSIX;
13 10     10   85 use Carp qw(croak carp);
  10         21  
  10         25933  
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 112   50 112   287 my $set = shift || 1024;
29 112 50       303 if (exists $DEFAULT_SUFFIXES{$set}) {
30 112 50       344 return @{$DEFAULT_SUFFIXES{$set}} if wantarray;
  112         557  
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 148     148   197 my $style = shift;
46 148 50       3184 if (exists $ROUND_FUNCTIONS{$style}) {
47 148         431 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 174     174   5293 my $seed = shift;
103 174         243 my %args;
104              
105             my %options;
106 174 100       1350 unless (defined $seed) { # use defaults
107 146         321 $options{BLOCK} = 1024;
108 146         287 $options{ROUND_STYLE} = 'ceil';
109 146         319 $options{ROUND_FUNCTION} = _round_function($options{ROUND_STYLE});
110 146         288 $options{ZERO} = '0';
111 146         244 $options{SI} = undef;
112 146         308 $options{PRECISION} = 1;
113 146         220 $options{PRECISION_CUTOFF} = 1;
114             #$options{SUFFIXES} = # deferred to the last minute when we know BLOCK, seek [**]
115 146         423 $options{UNIT} = undef;
116             }
117             # else { %options = %$seed } # this is set if @_!=0, down below
118              
119 174 100 66     723 if (@_==0) { # quick return for default values (no customized args)
    100          
120 65 50       243 return (defined $seed) ? $seed : \%options;
121             } elsif (@_==1 && ref $_[0]) { # \%args
122 27         37 %args = %{$_[0]};
  27         104  
123             } else { # arg1 => $val1, arg2 => $val2
124 82         235 %args = @_;
125             }
126              
127             # this is done here so this assignment/copy doesn't happen if @_==0
128 109 100       328 %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 109 100 100     1364 if ($args{block} ||
    100 100        
    100 100        
      100        
      100        
      100        
      100        
134             $args{block_size} ||
135             $args{base} ||
136             $args{bs}
137             ) {
138 75   66     649 my $block = $args{block} ||
139             $args{block_size} ||
140             $args{base} ||
141             $args{bs};
142 75 100 100     390 unless ($block==1000 || $block==1024 || $block==1_024_000) {
      100        
143 1         205 croak "invalid base: $block (should be 1024, 1000 or 1024000)";
144             }
145 74         151 $options{BLOCK} = $block;
146              
147             } elsif ($args{block_1024} ||
148             $args{base_1024} ||
149             $args{1024}) {
150              
151 3         7 $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 108 100       385 if ($args{round_function}) {
    100          
162 4 100       14 unless (ref $args{round_function} eq 'CODE') {
163 2         250 croak "round function ($args{round_function}) should be a code ref";
164             }
165 2         5 $options{ROUND_FUNCTION} = $args{round_function};
166 2   100     17 $options{ROUND_STYLE} = $args{round_style} || 'unknown';
167             } elsif ($args{round_style}) {
168 2         7 $options{ROUND_FUNCTION} = _round_function($args{round_style});
169 2         5 $options{ROUND_STYLE} = $args{round_style};
170             }
171              
172             # SI compatibility (mostly for parsing)
173 106 100       268 if ($args{si}) {
174 15         28 $options{SI} = 1;
175             }
176              
177             # suffixes => 1024 | 1000 | si_1024 | si_1000 | 1024000 | \@
178 106 100       230 if ($args{suffixes}) {
179 4 50       17 if (ref $args{suffixes} eq 'ARRAY') {
    0          
180 4         10 $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 106 100       325 if (defined $args{unit}) {
188 6         12 $options{UNIT} = $args{unit};
189             }
190              
191             # zero => undef | string
192 106 100       255 if (exists $args{zero}) {
193 11         23 $options{ZERO} = $args{zero};
194 11 100       31 if (defined $options{ZERO}) {
195 9         57 $options{ZERO} =~ s/%S/$options{SUFFIXES}->[0]/g
196             }
197             }
198              
199             # precision =>
200 106 100 66     294 if (exists $args{precision} and $args{precision} =~ /\A\d+\z/) {
201 1         4 $options{PRECISION} = $args{precision};
202             }
203              
204             # precision_cutoff =>
205 106 50 50     267 if (exists $args{precision_cutoff} and ($args{precision_cutoff} =~ /\A\d+\z/ or $args{precision_cutoff} = '-1')) {
      66        
206 1         4 $options{PRECISION_CUTOFF} = $args{precision_cutoff};
207             }
208              
209             # quiet => 1
210 106 50       363 if ($args{quiet}) {
211 0         0 $options{QUIET} = 1;
212             }
213              
214 106 100       263 if (defined $seed) {
215 25         88 %$seed = %options;
216 25         174 return $seed;
217             }
218 81         281 return \%options
219             }
220              
221             # NOTE. _format_bytes() SHOULD not change $options - NEVER.
222              
223             sub _format_bytes {
224 72     72   102 my $bytes = shift;
225 72 100       218 return undef unless defined $bytes;
226 69         103 my $options = shift;
227 69         415 my %options = %$options;
228              
229 69         206 local *human_round = $options{ROUND_FUNCTION};
230              
231 69 100 100     331 return $options{ZERO} if ($bytes==0 && defined $options{ZERO});
232              
233 58         100 my $block = $options{BLOCK};
234              
235             # if a suffix set was not specified, pick a default [**]
236 58 100       382 my @suffixes = $options{SUFFIXES} ? @{$options{SUFFIXES}} : _default_suffixes( ($options{SI} ? 'si_' : '') . $block);
  1 100       4  
237              
238             # WHAT ABOUT NEGATIVE NUMBERS: -1K ?
239 58         127 my $sign = '';
240 58 100       149 if ($bytes<0) {
241 3         6 $bytes = -$bytes;
242 3         8 $sign = '-';
243             }
244              
245 58         111 my $suffix = $suffixes[0];
246 58         98 my $x = $bytes;
247 58         80 my $magnitude = 0;
248 58 100       161 if($bytes >= $block) {
249             # return "$sign$bytes" if $bytes<$block;
250 29         46 do {
251 64         92 $x /= $block;
252 64         161 $magnitude++;
253             } while ( human_round($x, $options{PRECISION}) >= $block );
254 29 50       94 if($magnitude >= (0 + @suffixes)) {
255 0 0       0 carp "number too large (>= $block**$magnitude)" unless ($options{QUIET});
256             }
257 29         62 $suffix = $suffixes[$magnitude];
258             }
259             #$x = human_round( $x, $options{PRECISION} );
260              
261 58         191 $x = _precision_cutoff($x, $options);
262             #reasses encase the precision_cutoff caused the value to cross the block size
263 58 100       194 if($x >= $block) {
264 1         3 $x /= $block;
265 1         2 $magnitude++;
266 1 50       6 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         4 $x = _precision_cutoff($x, $options);
271             }
272              
273 58   100     249 my $unit = $options{UNIT} || '';
274              
275 58         589 return $sign . $x . $suffix . $unit;
276              
277             }
278              
279             sub _precision_cutoff {
280 59     59   91 my $bytes = shift;
281 59         82 my $options = shift;
282 59         323 my %options = %$options;
283 59 100 66     500 if ( $options{PRECISION_CUTOFF} != -1 and ( length( sprintf( "%d", $bytes ) ) > $options{PRECISION_CUTOFF} ) ) {
284 33         89 $bytes = sprintf( "%d", human_round( $bytes, 0 ) );
285             } else {
286 26         119 $bytes = sprintf( "%." . $options{PRECISION} . "f", human_round( $bytes, $options{PRECISION} ) );
287             }
288 59         245 return $bytes;
289             }
290              
291             sub _parse_bytes {
292 77     77   112 my $human = shift;
293 77         105 my $options = shift;
294 77         477 my %options = %$options;
295              
296 77 100 66     926 return 0 if( exists $options{ZERO} && ((!defined $options{ZERO} && !defined $human) || (defined $human && $human eq $options{ZERO})) );
      33        
297 64 100       175 return undef unless defined $human;
298              
299 61         78 my %suffix_mult;
300             my %suffix_block;
301 0         0 my $m;
302              
303 61 50       146 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 61 50 66     206 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 61         88 $m = 1;
314 61         70 foreach my $s (@{$DEFAULT_SUFFIXES{si_1000}}) {
  61         159  
315 549         942 $suffix_mult{$s} = $m;
316 549         790 $suffix_block{$s} = 1000;
317 549         1050 $m *= $suffix_block{$s};
318             }
319            
320 61         119 $m = 1;
321 61         79 foreach my $s (@{$DEFAULT_SUFFIXES{si_1024}}) {
  61         212  
322 549         1123 $suffix_mult{$s} = $m;
323 549         773 $suffix_block{$s} = 1024;
324 549         992 $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 61 100       200 if( !defined $options{SI} ) {
330 55         74 $m = 1;
331 55         126 foreach my $s (_default_suffixes( $options{BLOCK} )) {
332 451         910 $suffix_mult{$s} = $m;
333 451         723 $suffix_block{$s} = $options{BLOCK};
334 451         883 $m *= $suffix_block{$s};
335             }
336             }
337             }
338              
339 61         469 my ($sign, $int, $frac, $unit) = ($human =~ /^\s*(-?)\s*(\d*)(?:\.(\d*))?\s*(\D*)$/);
340              
341 61   100     271 $frac ||= 0;
342              
343             # print STDERR "S: $sign I: $int F: $frac U: $unit\n";
344              
345              
346 61         157 my $mult;
347             my $block;
348 61   100     275 my $u = $options{UNIT} || '';
349 61         308 foreach my $s (keys %suffix_block) {
350 746 100       10612 if( $unit =~ /^${s}${u}$/i ) {
351 61 100       170 $mult = ($sign eq '-' ? -1 : 1) * $suffix_mult{$s};
352 61         199 $block = $suffix_block{$s};
353 61         136 last;
354             }
355             }
356              
357 61 50       231 if( !defined $mult ) {
358 0         0 carp "Could not parse human readable byte value '$human'";
359 10     10   18625 use Data::Dumper;
  10         75277  
  10         4863  
360 0         0 print STDERR Dumper( %suffix_block );
361 0         0 return undef;
362             }
363              
364 61         198 my $bytes = int( ($int + ($frac / $block)) * $mult );
365              
366 61         1035 return $bytes;
367             }
368              
369              
370             # convert byte count (file size) to human readable format
371             sub format_bytes {
372 70     70 1 40013 my $bytes = shift;
373 70         192 my $options = _parse_args(undef, @_);
374             #use YAML; print Dump $options;
375 70         166 return _format_bytes($bytes, $options);
376             }
377              
378             # convert human readable format to byte count (file size)
379             sub parse_bytes {
380 75     75 1 22946 my $human = shift;
381 75         191 my $options = _parse_args(undef, @_);
382             #use YAML; print Dump $options;
383 75         186 return _parse_bytes($human, $options);
384             }
385              
386             ### the OO way
387              
388             # new()
389             sub new {
390 1     1 1 916 my $proto = shift;
391 1   33     12 my $class = ref $proto || $proto;
392 1         4 my $opts = _parse_args(undef, @_);
393 1         7 return bless $opts, $class;
394             }
395              
396             # set_options()
397             sub set_options {
398 1     1 1 3 my $self = shift;
399 1         5 return $self->_parse_args(@_);
400             }
401              
402             # format()
403             sub format {
404 2     2 1 1670 my $self = shift;
405 2         5 my $bytes = shift;
406 2         9 return _format_bytes($bytes, $self);
407             }
408              
409             # parse()
410             sub parse {
411 2     2 1 5 my $self = shift;
412 2         4 my $human = shift;
413 2         9 return _parse_bytes($human, $self);
414             }
415              
416             # the solution by COG in Filesys::DiskUsage
417             # convert size to human readable format
418             #sub _convert {
419             # defined (my $size = shift) || return undef;
420             # my $config = {@_};
421             # $config->{human} || return $size;
422             # my $block = $config->{'Human-readable'} ? 1000 : 1024;
423             # my @args = qw/B K M G/;
424             #
425             # while (@args && $size > $block) {
426             # shift @args;
427             # $size /= $block;
428             # }
429             #
430             # if ($config->{'truncate-readable'} > 0) {
431             # $size = sprintf("%.$config->{'truncate-readable'}f",$size);
432             # }
433             #
434             # "$size$args[0]";
435             #}
436             #
437             # not exact: 1024 => 1024B instead of 1K
438             # not nicely formatted => 1.00 instead of 1K
439              
440             1;
441              
442             __END__