File Coverage

lib/HATX.pm
Criterion Covered Total %
statement 102 102 100.0
branch 35 36 97.2
condition n/a
subroutine 17 17 100.0
pod 8 10 80.0
total 162 165 98.1


line stmt bran cond sub pod time code
1             package HATX;
2              
3 9     9   982549 use strict; use warnings; use utf8; use 5.10.0;
  9     9   15  
  9     9   297  
  9     9   42  
  9         41  
  9         451  
  9         57  
  9         23  
  9         85  
  9         423  
  9         105  
4 9     9   47 use Exporter 'import';
  9         18  
  9         304  
5 9     9   37 use Carp;
  9         20  
  9         849  
6 9     9   3974 use Clone qw/clone/;
  9         4221  
  9         10949  
7              
8             our $VERSION = '0.0.4';
9             our @EXPORT_OK = qw/hatx/;
10              
11             =head1 NAME
12              
13             HATX - A fluent interface for Hash and Array Transformations
14              
15             =cut
16             =head1 SYNOPSIS
17              
18             use HATX qw/hatx/;
19              
20             # Multiple versions of journal.html and projmgmt.html
21             my $files = [
22             'journal-v1.0.tar.gz 1201',
23             'journal-v1.1.tar.gz 1999',
24             'journal-v1.2.tar.gz 3100',
25             'projmgmt-v0.1.tar.gz 250',
26             'projmgmt-v0.2.tar.gz 350'
27             ];
28              
29             # Declare a helper object
30             my $max = { journal => '0.0', projmgmt => '0.0' };
31              
32             # hatx($obj) clones $obj; no clobbering
33             my $h = hatx($files)
34             # Internal object becomes equivalent to:
35             # [ 'journal-v1.0.tar.gz 1201',
36             # 'journal-v1.1.tar.gz 1999',
37             # 'journal-v1.2.tar.gz 3100',
38             # 'projmgmt-v0.1.tar.gz 250',
39             # 'projmgmt-v0.2.tar.gz 350' ]
40              
41             # Extract components: file, version, bytes
42             ->map(sub {
43             $_[0] =~ /(journal|projmgmt)-v(.+).tar.gz\s+(\d+)/;
44             return [$1, $2, $3]; # e.g. ['journal', '1.0', 1201]
45             })
46             # Internal object becomes equivalent to:
47             # [ ['journal', '1.0', 1201]
48             # ['journal', '1.1', 1999]
49             # ['journal', '1.2', 3100]
50             # ['projmgmt', '0.1', 250]
51             # ['projmgmt', '0.2', 350] ]
52              
53             # Accumulate file count and file sizes
54             ->apply(sub {
55             my ($v, $res) = @_;
56             $res->{count}++;
57             $res->{bytes} += $v->[2];
58             }, my $stats = { count => 0, bytes => 0 })
59             # Internal object unchanged
60             # The $stats variable becomes { count => 5, bytes => 6900 }
61              
62             # Determine the max version of each file, store into $max
63             ->apply(sub {
64             my ($v, $res) = @_;
65             my ($file, $ver, $size) = @$v;
66             if ($ver gt $res->{$file}) { $res->{$file} = $ver }
67             }, $max)
68             # Internal object unchanged
69             # $max variable becomes { journal => '1.2', projmgmt => '0.2' }
70              
71             # Keep only the max version
72             ->grep(sub {
73             my ($v, $res) = @_;
74             my ($file, $ver, $size) = @$v;
75             return $ver eq $res->{$file};
76             }, $max)
77             # Internal object reduced to:
78             # [ ['journal', '1.2', 3100]
79             # ['projmgmt', '0.2', 350] ]
80             ;
81              
82             =cut
83             =head1 METHODS
84             =cut
85              
86              
87             # Create from existing object without clobbering
88             sub from_obj {
89 30     30 0 54 my ($o, $obj) = @_;
90              
91 30 100       227 $o->{H} = clone($obj) if ref($obj) eq 'HASH';
92 30 100       206 $o->{A} = clone($obj) if ref($obj) eq 'ARRAY';
93              
94 30         48 return $o;
95             }
96              
97             # Default constructor
98             sub new {
99 33     33 0 182965 my $class = shift;
100 33         90 my $self = {H => undef, A => undef };
101 33         77 bless $self, $class;
102              
103 33         44 my $obj = shift;
104 33 100       144 $self->from_obj($obj) if defined $obj;
105              
106 33         190 return $self;
107             }
108              
109             =head2 hatx( $objref )
110              
111             DESCRIPTION
112              
113             Clone the given $objref to create a 'hatx' object instance. The
114             'hatx' object has an internal structure which is:
115              
116             One of: hashref | arrayref | undef
117              
118             This internal structure shall be called 'haref' in the rest of this
119             document.
120              
121             ARGUMENTS
122              
123             $objref - Reference to either a hash or an array
124              
125             RETURNS
126              
127             An instance of the HATX object.
128              
129             =cut
130             sub hatx {
131 30     30 1 1256858 return HATX->new(@_);
132             }
133              
134             =head2 to_obj()
135              
136             DESCRIPTION
137              
138             Converts the internal haref and returns it.
139              
140             ARGUMENTS
141              
142             None.
143              
144             RETURNS
145              
146             One of: hashref | arrayref | undef
147              
148             =cut
149             sub to_obj {
150 17     17 1 96 my $o = shift;
151              
152 17 100       52 return $o->{H} if defined $o->{H};
153 12 100       67 return $o->{A} if defined $o->{A};
154              
155             # If neither H or A is defined, return undef
156 1         2 return undef;
157             }
158              
159             =head2 map( $fn, [,@args] )
160              
161             DESCRIPTION
162              
163             Apply the given function, $fn, to each element of the internal
164             haref, replacing the entire haref.
165              
166             ARGUMENTS
167              
168             $fn - A user-provided function with a suitable signature.
169              
170             If internal haref is a hashref, $fn should have signature:
171              
172             $fn->($hkey_s, $hval_s [,@args]) returning ($hkey_t, $hval_t)
173              
174             WHERE
175             $hkey_s Key of source hashref pair
176             $hval_s Value of source hashref pair
177             @args Optional user variables
178             $hkey_t Key of target hashref pair
179             $hval_t Value of target hashref pair
180              
181             If the internal haref is an arrayref, $fn should have the signature:
182              
183             $fn->($val_s [,@args]) returning ($val_t)
184              
185             WHERE
186             $val_s An element of the source arrayref
187             @args Optional user variables
188             $val_t An element of the target arrayref
189              
190             @args - Optional arguments that are passed to $fn
191              
192             RETURNS
193              
194             The hatx object with the target haref.
195              
196             =cut
197             sub map {
198 5     5 1 6 my $o = shift;
199 5         7 my $fn = shift; # H: fn->($key,$val)
200             # A: fn->($val)
201 5         7 my @args = @_;
202              
203 5 100       27 if (defined($o->{H})) {
204 2         3 my $new_H = {};
205 2         2 foreach my $k (keys %{$o->{H}}) {
  2         5  
206 6         15 my ($k2,$v2) = $fn->($k,$o->{H}{$k},@args);
207 6         26 $new_H->{$k2} = $v2;
208             }
209 2         4 $o->{H} = $new_H;
210             }
211 5 100       9 if (defined($o->{A})) {
212 3         16 my $new_A = [];
213 3         4 foreach my $v (@{$o->{A}}) {
  3         7  
214 9         33 push @$new_A, $fn->($v,@args);
215             }
216 3         13 $o->{A} = $new_A;
217             }
218              
219 5         16 return $o;
220             }
221              
222             =head2 grep( $fn [,@args] )
223              
224             DESCRIPTION
225              
226             Retain only elements of the haref where $fn returns true.
227              
228             ARGUMENTS
229              
230             $fn - A user-provided function with a suitable signature.
231              
232             If internal haref is a hashref, $fn should have signature:
233              
234             $fn->($hkey_s, $hval_s [,@args]) returning ($hkey_t, $hval_t)
235              
236             WHERE
237             $hkey_s Key of source hashref pair
238             $hval_s Value of source hashref pair
239             @args Optional user variables
240             $hkey_t Key of target hashref pair
241             $hval_t Value of target hashref pair
242              
243             If the internal haref is an arrayref, $fn should have the signature:
244              
245             $fn->($val_s [,@args]) returning ($val_t)
246              
247             WHERE
248             $val_s An element of the source arrayref
249             @args Optional user variables
250             $val_t An element of the target arrayref
251              
252             @args - Optional arguments that are passed to $fn
253              
254             RETURNS
255              
256             The hatx object with elements containing only 'grepped' elements.
257              
258             =cut
259             sub grep {
260 4     4 1 8 my $o = shift;
261 4         4 my $fn = shift; # H: fn->($key,$val) -> BOOL
262             # A: fn->($val) -> BOOL
263 4         10 my @args = @_;
264              
265 4 100       10 if (defined($o->{H})) {
266 2         4 my $new_H = {};
267 2         3 foreach my $k (keys %{$o->{H}}) {
  2         8  
268 6 100       43 delete $o->{H}{$k} unless $fn->($k,$o->{H}{$k},@args);
269             }
270             }
271 4 100       21 if (defined($o->{A})) {
272 2         11 my $new_A = [];
273 2         4 foreach my $v (@{$o->{A}}) {
  2         7  
274 6 100       30 push @$new_A, $v if $fn->($v,@args);
275             }
276 2         12 $o->{A} = $new_A;
277             }
278              
279 4         20 return $o;
280             }
281              
282             =head2 sort( $fn )
283              
284             DESCRIPTION
285              
286             Sorts contents of arrayref. Hashrefs are unmodified.
287              
288             ARGUMENTS
289              
290             $fn - A function reference with prototype ($$) i.e. taking two
291             arguments. See https://perldoc.perl.org/functions/sort.
292              
293             Examples of $fn:
294              
295             sub ($$) { $_[1] cmp $_[0] } # Sort descending alphabetically
296             sub ($$) { $_[0] <=> $_[1] } # Sort ascending numerically
297             sub ($$) { $_[1] <=> $_[0] } # Sort descending numerically
298              
299             RETURNS
300              
301             The sorted hatx object.
302              
303             =cut
304             sub sort ($&) {
305 5     5 1 7 my $o = shift;
306 5         5 my $fn = shift; # A: $fn is a BLOCK
307              
308 5 100       9 if (defined($o->{H})) {
309             # do nothing
310             }
311 5 100       8 if (defined($o->{A})) {
312 3         9 $o->{A} = defined $fn ? [ sort $fn @{$o->{A}} ]
313 4 100       9 : [ sort @{$o->{A}} ];
  1         9  
314             }
315              
316 5         41 return $o;
317             }
318              
319             =head2 to_href( $fn [,@args] )
320              
321             DESCRIPTION
322              
323             Convert internal arrayref to hashref using the given function, $fn,
324             fn and optionally additional arguments, @args, as needed.
325              
326             ARGUMENTS
327              
328             $fn - A user-provided function reference with signature:
329              
330             $fn->($val [,@args]) returning ($hkey, $hval)
331              
332             WHERE
333             $val An element of the source arrayref
334             @args Optional user variables
335             $hkey Key of target hashref pair
336             $hval Value of target hashref pair
337              
338             @args - Optional arguments that are passed to $fn
339              
340             RETURNS
341              
342             The hatx object where the internal structure is a hashref.
343              
344             =cut
345             sub to_href {
346 1     1 1 3 my ($o,$fn) = @_;
347 1         4 $o->map($fn);
348 1 50       11 carp 'HATX/to_href: Not an array' unless ref($o->{A}) eq 'ARRAY';
349 1         2 $o->{H} = {@{$o->{A}}};
  1         6  
350 1         3 $o->{A} = undef;
351              
352 1         5 return $o;
353             }
354              
355             =head2 to_aref( $fn [,@args] )
356              
357             DESCRIPTION
358              
359             Convert internal hashref to arrayref using the given function, $fn
360             and optionally additional arguments, @args, as needed.
361              
362             ARGUMENTS
363              
364             $fn - A user-provided function reference with signature:
365              
366             $fn->($hkey, $hval [,@args]) returning ($val)
367              
368             WHERE
369             $hkey Key of source hashref pair
370             $hval Value of source hashref pair
371             @args Optional user variables
372             $val An element of the target arrayref
373              
374             @args - Optional arguments that are passed to $fn
375              
376             RETURNS
377              
378             The hatx object where the internal structure is a hashref.
379              
380             =cut
381             sub to_aref {
382 3     3 1 10 my ($o,$fn,@args) = @_;
383              
384 3 100       8 if (defined($o->{H})) {
385 2         3 my $new_A = [];
386 2         3 foreach my $k (keys %{$o->{H}}) {
  2         8  
387 6         33 push @$new_A, $fn->($k,$o->{H}{$k},@args);
388             }
389 2         11 $o->{A} = $new_A;
390 2         6 $o->{H} = undef;
391             } else {
392 1         219 croak 'HATX/to_aref: No hashref to transform.';
393             }
394              
395 2         8 return $o;
396             }
397              
398             =head2 apply( $fn [,@args] )
399              
400             DESCRIPTION
401              
402             Apply the given function, $fn to each item in the haref. The haref
403             is unchanged. Typically used to find aggregate values e.g. max/min or
404             totals which are then stored into @args.
405              
406             ARGUMENTS
407              
408             $fn - A user-provided function with a suitable signature.
409              
410             If internal haref is a hashref, $fn should have signature:
411              
412             $fn->($hkey_s, $hval_s [,@args]) with no return values
413              
414             WHERE
415             $hkey_s Key of source hashref pair
416             $hval_s Value of source hashref pair
417             @args Optional user variables
418              
419             If the internal haref is an arrayref, $fn should have the signature:
420              
421             $fn->($val_s [,@args]) with no return values
422              
423             WHERE
424             $val_s An element of the source arrayref
425             @args Optional user variables
426              
427             @args - Optional arguments that are passed to $fn
428              
429             RETURNS
430              
431             The same hatx object.
432              
433             =cut
434             sub apply {
435 4     4 1 6 my ($o,$fn,@args) = @_;
436              
437 4 100       6 if (defined($o->{H})) {
438             # Clone prevents modification to $o->{H}
439 2         11 my $href = clone($o->{H});
440 2         4 foreach my $k (keys %$href) {
441 6         18 $fn->($k,$href->{$k},@args);
442             }
443             }
444 4 100       10 if (defined($o->{A})) {
445             # Clone prevents modification to $o->{A}
446 2         12 my $aref = clone($o->{A});
447 2         3 foreach my $v (@$aref) {
448 6         15 $fn->($v,@args);
449             }
450             }
451              
452 4         15 return $o;
453             }
454              
455             1;
456             __END__