File Coverage

blib/lib/Text/Unpack/Auto.pm
Criterion Covered Total %
statement 43 43 100.0
branch 10 10 100.0
condition n/a
subroutine 12 12 100.0
pod 2 6 33.3
total 67 71 94.3


line stmt bran cond sub pod time code
1 3     3   326735 use strict;
  3         6  
  3         100  
2 3     3   12 use warnings;
  3         5  
  3         223  
3             package Text::Unpack::Auto;
4              
5             # ABSTRACT: automatically generates unpack strings
6              
7 3     3   1180 use parent 'Exporter'; # inherit all of Exporter's methods
  3         838  
  3         15  
8 3     3   202 use List::Util qw(reduce pairmap max min);
  3         9  
  3         2341  
9              
10             our @EXPORT = qw(guess_unpack auto_unpack);
11             our @EXPORT_OK = qw(); # symbols to export on reques
12              
13 19     19 0 227008 sub rle_encode { shift =~ s/(.)\1*/$1 . ":" . length($&) . " "/grse }
  91         438  
14              
15 2     2 0 22 sub rle_decode { shift =~ s/(\d+):(.) /$2 x $1/grse }
  4         31  
16              
17 90 100   90 0 315 sub rle_to_unpack { join '', pairmap { ($a ? 'a' : 'x') . $b } (map { split ":", $_ } split ' ', shift) }
  18     18   120  
  90         321  
18              
19             sub close_gaps {
20 18     18 0 161248 my ($str, $n) = @_;
21 18 100       52 return $str unless $n > 1;
22 16         23 my $min_gap = $n - 1;
23 16         298 $str =~ s/
24             (?<=1) # preceded by a data column (lookbehind)
25             ( # capture the gap
26             0{1,$min_gap} # whitespace columns, up to $min_gap wide
27             ) # gaps wider than this are real column separators
28             (?=1) # followed by a data column (lookahead)
29 9         35 /1 x length($1)/gex;
30 16         104 return $str;
31             }
32              
33             sub guess_unpack {
34 17 100   17 1 260678 my $opts = ref $_[0] eq 'HASH' ? shift : {};
35              
36 17         55 my @zeros = map { my $z = s/\S/1/gr; $z =~ s/\s/0/g; $z } @_;
  36         303  
  36         187  
  36         100  
37              
38 17     19   219 my $result = reduce { $a | $b } @zeros;
  19         70  
39 17 100       123 $result = close_gaps($result, $opts->{minimum_gap}) if ($opts->{minimum_gap});
40 17         61 my $unpack = rle_to_unpack(rle_encode($result));
41 17         130 return $unpack;
42             }
43              
44             sub auto_unpack {
45 7 100   7 1 31057 my $opts = ref $_[0] eq 'HASH' ? shift : {};
46              
47 7         31 my @lines = @_;
48 7         32 my $unpack = guess_unpack($opts, @lines);
49 7         29 my $ml = max map { length($_) } @lines;
  16         54  
50              
51 7         30 return map { [ map { s/^\s+|\s+$//gr } unpack $unpack, sprintf "%-${ml}s", $_ ] } @lines
  16         103  
  60         283  
52             }
53              
54             1;
55              
56             =head1 NAME
57              
58             Text::Unpack::Auto - automatically generate unpack strings from fixed-width text
59              
60             =head1 SYNOPSIS
61              
62             use Text::Unpack::Auto;
63              
64             my @rows = auto_unpack(@lines);
65             for my $row (@rows) {
66             say join ', ', @$row;
67             }
68              
69             my $fmt = guess_unpack(@lines);
70              
71             my @rows = auto_unpack({ minimum_gap => 3 }, @lines);
72              
73             =head1 DESCRIPTION
74              
75             Detects fixed-width column boundaries in plain text and unpacks lines into
76             fields.
77              
78             =head1 FUNCTIONS
79              
80             L exports L and L by
81             default.
82              
83             =head2 guess_unpack
84              
85             my $fmt = guess_unpack(@lines);
86             my $fmt = guess_unpack(\%opts, @lines);
87              
88             Returns an L template string derived from the column
89             boundaries detected in C<@lines>.
90              
91             =head2 auto_unpack
92              
93             my @rows = auto_unpack(@lines);
94             my @rows = auto_unpack(\%opts, @lines);
95              
96             Unpacks each line into an arrayref of trimmed fields. Returns one arrayref per
97             input line.
98              
99             =head2 Options
100              
101             =over 2
102              
103             =item minimum_gap
104              
105             minimum_gap => 3
106              
107             Gaps narrower than this are treated as part of the surrounding column rather
108             than as column separators.
109              
110             =back
111              
112              
113             =head1 AUTHOR
114              
115             Simone Cesano
116              
117             =head1 COPYRIGHT AND LICENSE
118              
119             Copyright (C) 2026 Simone Cesano
120              
121             This library is free software; you may redistribute it and/or modify it under
122             the same terms as Perl itself.
123              
124             =cut
125              
126             1;