File Coverage

blib/lib/Number/Bytes/Human.pm
Criterion Covered Total %
statement 162 181 89.5
branch 74 92 80.4
condition 47 67 70.1
subroutine 16 16 100.0
pod 6 6 100.0
total 305 362 84.2


line stmt bran cond sub pod time code
1             package Number::Bytes::Human;
2              
3 10     10   760116 use strict;
  10         17  
  10         224  
4 10     10   33 use warnings;
  10         11  
  10         601  
5              
6             our $VERSION = '0.10';
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   34 use Carp qw(croak carp);
  10         15  
  10         12932  
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 114   50 114   191 my $set = shift || 1024;
29 114 50       188 if (exists $DEFAULT_SUFFIXES{$set}) {
30 114 50       166 return @{$DEFAULT_SUFFIXES{$set}} if wantarray;
  114         310  
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 150     150   115 my $style = shift;
46 150 50       253 if (exists $ROUND_FUNCTIONS{$style}) {
47 150         242 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 176     176   2793 my $seed = shift;
103 176         151 my %args;
104              
105             my %options;
106 176 100       348 unless (defined $seed) { # use defaults
107 148         231 $options{BLOCK} = 1024;
108 148         145 $options{ROUND_STYLE} = 'ceil';
109 148         218 $options{ROUND_FUNCTION} = _round_function($options{ROUND_STYLE});
110 148         152 $options{ZERO} = '0';
111 148         125 $options{SI} = undef;
112 148         130 $options{PRECISION} = 1;
113 148         119 $options{PRECISION_CUTOFF} = 1;
114             #$options{SUFFIXES} = # deferred to the last minute when we know BLOCK, seek [**]
115 148         264 $options{UNIT} = undef;
116             }
117             # else { %options = %$seed } # this is set if @_!=0, down below
118              
119 176 100 66     480 if (@_==0) { # quick return for default values (no customized args)
    100          
120 67 50       181 return (defined $seed) ? $seed : \%options;
121             } elsif (@_==1 && ref $_[0]) { # \%args
122 27         33 %args = %{$_[0]};
  27         87  
123             } else { # arg1 => $val1, arg2 => $val2
124 82         137 %args = @_;
125             }
126              
127             # this is done here so this assignment/copy doesn't happen if @_==0
128 109 100       199 %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 66     691 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 75   66     239 $args{bs};
142 75 100 100     227 unless ($block==1000 || $block==1024 || $block==1_024_000) {
      100        
143 1         231 croak "invalid base: $block (should be 1024, 1000 or 1024000)";
144             }
145 74         79 $options{BLOCK} = $block;
146              
147             } elsif ($args{block_1024} ||
148             $args{base_1024} ||
149             $args{1024}) {
150              
151 3         6 $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       209 if ($args{round_function}) {
    100          
162 4 100       10 unless (ref $args{round_function} eq 'CODE') {
163 2         171 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         5 $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 106 100       151 if ($args{si}) {
174 15         12 $options{SI} = 1;
175             }
176              
177             # suffixes => 1024 | 1000 | si_1024 | si_1000 | 1024000 | \@
178 106 100       141 if ($args{suffixes}) {
179 4 50       13 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 106 100       147 if (defined $args{unit}) {
188 6         8 $options{UNIT} = $args{unit};
189             }
190              
191             # zero => undef | string
192 106 100       145 if (exists $args{zero}) {
193 11         10 $options{ZERO} = $args{zero};
194 11 100       20 if (defined $options{ZERO}) {
195 9         22 $options{ZERO} =~ s/%S/$options{SUFFIXES}->[0]/g
196             }
197             }
198              
199             # precision =>
200 106 100 66     184 if (exists $args{precision} and $args{precision} =~ /\A\d+\z/) {
201 1         2 $options{PRECISION} = $args{precision};
202             }
203              
204             # precision_cutoff =>
205 106 50 50     154 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 106 50       143 if ($args{quiet}) {
211 0         0 $options{QUIET} = 1;
212             }
213              
214 106 100       143 if (defined $seed) {
215 25         48 %$seed = %options;
216 25         120 return $seed;
217             }
218 81         161 return \%options
219             }
220              
221             # NOTE. _format_bytes() SHOULD not change $options - NEVER.
222              
223             sub _format_bytes {
224 73     73   55 my $bytes = shift;
225 73 100       126 return undef unless defined $bytes;
226 70         51 my $options = shift;
227 70         242 my %options = %$options;
228              
229 70         164 local *human_round = $options{ROUND_FUNCTION};
230              
231 70 100 100     227 return $options{ZERO} if ($bytes==0 && defined $options{ZERO});
232              
233 59         48 my $block = $options{BLOCK};
234              
235             # if a suffix set was not specified, pick a default [**]
236 59 100       186 my @suffixes = $options{SUFFIXES} ? @{$options{SUFFIXES}} : _default_suffixes( ($options{SI} ? 'si_' : '') . $block);
  1 100       2  
237              
238             # WHAT ABOUT NEGATIVE NUMBERS: -1K ?
239 59         82 my $sign = '';
240 59 100       89 if ($bytes<0) {
241 3         3 $bytes = -$bytes;
242 3         5 $sign = '-';
243             }
244              
245 59         47 my $suffix = $suffixes[0];
246 59         38 my $x = $bytes;
247 59         46 my $magnitude = 0;
248 59 100       92 if($bytes >= $block) {
249             # return "$sign$bytes" if $bytes<$block;
250             do {
251 67         58 $x /= $block;
252 67         93 $magnitude++;
253 30         27 } while ( human_round($x, $options{PRECISION}) >= $block );
254 30 50       62 if($magnitude >= (0 + @suffixes)) {
255 0 0       0 carp "number too large (>= $block**$magnitude)" unless ($options{QUIET});
256             }
257 30         41 $suffix = $suffixes[$magnitude];
258             }
259             #$x = human_round( $x, $options{PRECISION} );
260              
261 59         73 $x = _precision_cutoff($x, $options);
262             #reasses encase the precision_cutoff caused the value to cross the block size
263 59 100       135 if($x >= $block) {
264 1         1 $x /= $block;
265 1         1 $magnitude++;
266 1 50       3 if($magnitude >= (0 + @suffixes)) {
267 0 0       0 carp "number too large (>= $block**$magnitude)" unless ($options{QUIET});
268             }
269 1         1 $suffix = $suffixes[$magnitude];
270 1         2 $x = _precision_cutoff($x, $options);
271             }
272              
273 59   100     162 my $unit = $options{UNIT} || '';
274              
275 59         387 return $sign . $x . $suffix . $unit;
276              
277             }
278              
279             sub _precision_cutoff {
280 60     60   46 my $bytes = shift;
281 60         42 my $options = shift;
282 60         159 my %options = %$options;
283 60 100 66     355 if ( $options{PRECISION_CUTOFF} != -1 and ( length( sprintf( "%d", $bytes ) ) > $options{PRECISION_CUTOFF} ) ) {
284 33         47 $bytes = sprintf( "%d", human_round( $bytes, 0 ) );
285             } else {
286 27         59 $bytes = sprintf( "%." . $options{PRECISION} . "f", human_round( $bytes, $options{PRECISION} ) );
287             }
288 60         135 return $bytes;
289             }
290              
291             sub _parse_bytes {
292 78     78   63 my $human = shift;
293 78         56 my $options = shift;
294 78         273 my %options = %$options;
295              
296 78 100 66     630 return 0 if( exists $options{ZERO} && ((!defined $options{ZERO} && !defined $human) || (defined $human && $human eq $options{ZERO})) );
      33        
297 65 100       109 return undef unless defined $human;
298              
299 62         50 my %suffix_mult;
300             my %suffix_block;
301 0         0 my $m;
302              
303 62 50       86 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 62 50 66     128 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 62         50 $m = 1;
314 62         42 foreach my $s (@{$DEFAULT_SUFFIXES{si_1000}}) {
  62         110  
315 558         476 $suffix_mult{$s} = $m;
316 558         422 $suffix_block{$s} = 1000;
317 558         572 $m *= $suffix_block{$s};
318             }
319            
320 62         53 $m = 1;
321 62         48 foreach my $s (@{$DEFAULT_SUFFIXES{si_1024}}) {
  62         73  
322 558         496 $suffix_mult{$s} = $m;
323 558         417 $suffix_block{$s} = 1024;
324 558         515 $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 62 100       102 if( !defined $options{SI} ) {
330 56         40 $m = 1;
331 56         65 foreach my $s (_default_suffixes( $options{BLOCK} )) {
332 460         415 $suffix_mult{$s} = $m;
333 460         351 $suffix_block{$s} = $options{BLOCK};
334 460         431 $m *= $suffix_block{$s};
335             }
336             }
337             }
338              
339 62         333 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 62         56 my $mult;
345 62   100     466 my $u = $options{UNIT} || '';
346 62         193 foreach my $s (keys %suffix_block) {
347 721 100       3337 if( $unit =~ /^${s}${u}$/i ) {
348 62 100       109 $mult = ($sign eq '-' ? -1 : 1) * $suffix_mult{$s};
349 62         88 last;
350             }
351             }
352              
353 62 50       142 if( !defined $mult ) {
354 0         0 carp "Could not parse human readable byte value '$human'";
355 10     10   5395 use Data::Dumper;
  10         41719  
  10         2668  
356 0         0 print STDERR Dumper( %suffix_block );
357 0         0 return undef;
358             }
359              
360 62         121 my $bytes = int( $k * $mult );
361              
362 62         624 return $bytes;
363             }
364              
365              
366             # convert byte count (file size) to human readable format
367             sub format_bytes {
368 71     71 1 17659 my $bytes = shift;
369 71         118 my $options = _parse_args(undef, @_);
370             #use YAML; print Dump $options;
371 71         110 return _format_bytes($bytes, $options);
372             }
373              
374             # convert human readable format to byte count (file size)
375             sub parse_bytes {
376 76     76 1 10959 my $human = shift;
377 76         133 my $options = _parse_args(undef, @_);
378             #use YAML; print Dump $options;
379 76         118 return _parse_bytes($human, $options);
380             }
381              
382             ### the OO way
383              
384             # new()
385             sub new {
386 1     1 1 307 my $proto = shift;
387 1   33     6 my $class = ref $proto || $proto;
388 1         2 my $opts = _parse_args(undef, @_);
389 1         2 return bless $opts, $class;
390             }
391              
392             # set_options()
393             sub set_options {
394 1     1 1 1 my $self = shift;
395 1         2 return $self->_parse_args(@_);
396             }
397              
398             # format()
399             sub format {
400 2     2 1 262 my $self = shift;
401 2         2 my $bytes = shift;
402 2         3 return _format_bytes($bytes, $self);
403             }
404              
405             # parse()
406             sub parse {
407 2     2 1 2 my $self = shift;
408 2         2 my $human = shift;
409 2         3 return _parse_bytes($human, $self);
410             }
411              
412             # the solution by COG in Filesys::DiskUsage
413             # convert size to human readable format
414             #sub _convert {
415             # defined (my $size = shift) || return undef;
416             # my $config = {@_};
417             # $config->{human} || return $size;
418             # my $block = $config->{'Human-readable'} ? 1000 : 1024;
419             # my @args = qw/B K M G/;
420             #
421             # while (@args && $size > $block) {
422             # shift @args;
423             # $size /= $block;
424             # }
425             #
426             # if ($config->{'truncate-readable'} > 0) {
427             # $size = sprintf("%.$config->{'truncate-readable'}f",$size);
428             # }
429             #
430             # "$size$args[0]";
431             #}
432             #
433             # not exact: 1024 => 1024B instead of 1K
434             # not nicely formatted => 1.00 instead of 1K
435              
436             1;
437              
438             __END__