]> git.donarmstrong.com Git - bin.git/blob - txt2xls
add mutt alias which executes neomutt if that exists
[bin.git] / txt2xls
1 #! /usr/bin/perl
2 # txt2xls turns text files into excel workbooks, and is released
3 # under the terms of the GPL version 2, or any later version, at your
4 # option. See the file README and COPYING for more information.
5 # Copyright 2008 by Don Armstrong <don@donarmstrong.com>.
6 # $Id: perl_script 1153 2008-04-08 00:04:20Z don $
7
8
9 use warnings;
10 use strict;
11
12 use Getopt::Long;
13 use Pod::Usage;
14
15 =head1 NAME
16
17 txt2xls - Turns a (set of) text file(s) into an excel workbook
18
19 =head1 SYNOPSIS
20
21  [options]
22
23  Options:
24   --tsv, -t tab separated value mode (Default)
25   --ssv, -s space separated value mode
26   --csv, -c comma separated value mode
27   --r-mode, -r R mode (Default)
28   --auto-format Auto format (Default)
29   --sci-format Scientific format formatting string (Default 0.00E+0)
30   --max-digits Maximum digits to use for auto-format (Default 4)
31   --debug, -d debugging level (Default 0)
32   --help, -h display this help
33   --man, -m display manual
34
35 =head1 OPTIONS
36
37 =over
38
39 =item B<--auto-format>
40
41 Attempt to automatically format the excel file. Currently, this does
42 nothing for non-numeric entries. For numeric entries, if the number is
43 very large (> 9999), or less than 0.001, but not 0, the cell is put
44 into scientific format (B<--sci-format>, default C<0.00E+0>).
45 Otherwise, the cell is formatted to use at maximum B<--max-digits>
46 (default 4) digits.
47
48 To disable, use B<--no-auto-format>
49
50 =item B<--sci-format>
51
52 Excel format string to use for scientific format. See
53 L<http://office.microsoft.com/en-us/excel-help/number-format-codes-HP005198679.aspx>
54 for details. (Default C<'0.00E+0'>)
55
56 =item B<--max-digits>
57
58 Maximum number digits to display for non-scientific formats. (Default 4)
59
60 =item B<--debug, -d>
61
62 Debug verbosity. (Default 0)
63
64 =item B<--help, -h>
65
66 Display brief useage information.
67
68 =item B<--man, -m>
69
70 Display this manual.
71
72 =back
73
74 =head1 EXAMPLES
75
76
77 =cut
78
79
80 use vars qw($DEBUG);
81
82 use Text::CSV;
83 use Spreadsheet::WriteExcel;
84 use Scalar::Util qw(looks_like_number);
85 use POSIX qw(floor);
86
87 my %options = (debug           => 0,
88                help            => 0,
89                man             => 0,
90                auto_format     => 1,
91                sci_format      => '0.00E+0',
92                max_digits      => 4,
93                remove_name     => [],
94                );
95
96 GetOptions(\%options,
97            'tsv|t',
98            'ssv|s',
99            'csv|c',
100            'auto_format|auto-format!',
101            'sci_format|sci-format=s',
102            'max_digits|max-digits=i',
103            'rmode|r-mode|r!',
104            'remove_name|remove-name=s@',
105            'output|output_file|output-file=s',
106            'debug|d+','help|h|?','man|m');
107
108 pod2usage() if $options{help};
109 pod2usage({verbose=>2}) if $options{man};
110
111 $DEBUG = $options{debug};
112
113 my @USAGE_ERRORS;
114 if (0 == grep {exists $options{$_}} qw(tsv ssv csv)) {
115     $options{tsv} = 1
116 }
117 if (1 < grep {exists $options{$_}} qw(tsv ssv csv)) {
118      push @USAGE_ERRORS,"You can only pass one of --tsv, --ssv, or --csv";
119 }
120
121 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
122
123 if (not @ARGV) {
124     # we'll use this as a special indicator to read stdin
125     push @ARGV,undef;
126 }
127
128 my $sep_char = "\t";
129 if ($options{csv}) {
130     $sep_char = ',';
131 }
132 elsif ($options{ssv}) {
133     $sep_char = ' ';
134 }
135
136 if (not @{$options{remove_name}}) {
137     $options{remove_name} = ['.+\/',
138                             ];
139 }
140
141 my %wb_formats = ();
142 my $csv = Text::CSV->new({sep_char=>$sep_char});
143 my $wb;
144 if (defined $options{output}) {
145     $wb =  Spreadsheet::WriteExcel->new($options{output});
146 } else {
147     $wb = Spreadsheet::WriteExcel->new(\*STDOUT);
148 }
149
150 for my $file (@ARGV) {
151     my $fh;
152     if (not defined $file) {
153         $fh = \*STDIN;
154         $file = "STDIN";
155     }
156     else {
157         open($fh,'<:encoding(utf8)',$file) or
158             die "Unable to open $file for reading: $!";
159     }
160     my $ws_name = $file;
161     foreach my $remove (@{$options{remove_name}}) {
162         $ws_name =~ s{$remove}{}g;
163     }
164     $ws_name =~ s{\.[^\.]+$}{}g;
165     $ws_name =~ s/_+/ /g;
166     $ws_name =~ s{[\]:*?\/\] ]+}{ }g;
167     $ws_name =~ s{(?:^\s+|\s+$)}{}g;
168     $ws_name =~ s{^(.{0,31}).*$}{$1};
169     my $ws = $wb->add_worksheet($ws_name) or
170         die "Unable to add worksheet to workbook";
171     my $row = 1;
172     my @header_row;
173     my $overflow = 0;
174     my $r_mode = $options{r_mode} // 0;
175     # set to 1 if we have attempted to autodetect R mode
176     my $r_mode_autodetected = 0;
177     while (<$fh>) {
178         chomp;
179         # parse the line
180         die "Unable to parse line $. of $file: ".$csv->error_diag() unless $csv->parse($_);
181         my @row = $csv->fields();
182         if ($row==1 and not $r_mode_autodetected) {
183             @header_row = @row;
184             $row++;
185             next;
186         }
187         if ($row==2 and not $r_mode_autodetected) {
188             $r_mode_autodetected = 1;
189             if (@row == (@header_row+1)) {
190                 $r_mode = 1 unless exists $options{r_mode} and defined $options{r_mode};
191             }
192             if ($r_mode) {
193                 # R doesn't output headers for rownames
194                 unshift @header_row,'';
195             }
196             output_row(\@header_row,1,$ws,$wb,\%wb_formats,\%options);
197         }
198         if ($row > 65536) { # ok, we're going to overflow here
199             my $t_ws_name = $ws_name;
200             my $maxlen = 31-length('.'.$overflow);
201             $t_ws_name =~ s{^(.{0,$maxlen}).*$}{$1};
202             $ws = $wb->add_worksheet($t_ws_name.'.'.$overflow);
203             $overflow++;
204             $row=1;
205             output_row(\@header_row,$row,$ws,$wb,\%wb_formats,\%options);
206             $row++;
207         }
208         if ($row==1) {
209             @header_row = @row;
210         }
211         output_row(\@row,$row,$ws,$wb,\%wb_formats,\%options);
212         $row++;
213     }
214 }
215
216 sub output_row{
217     my ($data,$row,$ws,$wb,$formats,$options) = @_;
218     my @columns = ('A'..'Z','AA'..'ZZ');
219     for my $i (0..$#{$data}) {
220         my $format;
221         if ($options->{auto_format}) {
222             if (looks_like_number($data->[$i])) {
223                 my $format_string;
224                 # use scientific format?
225                 if ($data->[$i] != 0 && (abs($data->[$i]) > 9999 ||
226                                          abs($data->[$i]) < 0.001)) {
227                     $format_string = $options->{sci_format};
228                 } else {
229                     $format_string = '0';
230                     my $digits = length(floor(abs($data->[$i])));
231                     if ($options->{max_digits} - $digits > 0 and
232                         abs($data->[$i]) != abs(floor($data->[$i]))) {
233                         # if there are digits left over, use them for
234                         # decimal places, but don't require them
235                         $format_string = $format_string.'.'.('#'x($options->{max_digits} - $digits))
236                     }
237                 }
238                 if (not exists $formats->{$format_string}) {
239                     $formats->{$format_string} =
240                         $wb->add_format();
241                     $formats->{$format_string}->
242                         set_num_format($format_string);
243                 }
244                 $format = $formats->{$format_string};
245             }
246         }
247         $ws->write($columns[$i].$row,$data->[$i],defined $format?$format:())
248     }
249 }
250
251 __END__