# $Source: /usr/pkg/lib/perl5/site_perl/5.8.0/Text/CSV/Munge.pm $ # $Date: 2006-08-22 $ package Text::CSV::Munge; use strict; use warnings; use Carp qw(carp croak); use Text::CSV::Simple; use File::Basename; # Sorry, Perl::Critic, but gotta do it. require Exporter; our @ISA = ('Exporter'); our @EXPORT = qw( quote_neatly ); our ($VERSION) = '$Revision: 0.01 $' =~ m{ \$Revision: \s+ (\S+) }xm; # Confuses editor syntax highlighting. our $ASCII_POUND = "\043"; our $ASCII_SQUOT = "\047"; ################## # OBJECT SECTION # ################## # Create new object. sub new { ref( my $class = shift ) and croak 'Oops! Method new() is class, not instance.'; my $self = {}; bless $self, $class; $self->set_keys('Column Names', []); # An array of names for each channel. $self->set_keys('Column Data Arefs', []); # An array of arrays, one array for each channel. $self->set_keys('Column Hrefs', []); # An array of hashes, one hash for each channel. $self->set_keys('Verbosity Level', 1); # 0 = Quiet, 1 = Info, 2 = Diagnostic. $self->set_keys('Info File Flag', 0); # Boolean $self->set_keys('Field Separator', qq{,}); # May use comma, tab, semicolon, etc. return $self; } # Change defaults and input info. sub set_keys { ref( my $self = shift ) or croak 'Oops! Method set_keys() is instance, not class.'; my %user_defs = @_; while ( my ($key, $value) = each %user_defs ) { $key = constrain_key( $key ); # Enforce key naming rule. $self->{$key} = $value; } return 'Pthhht! to Perl::Critic'; } # Get back stored info. sub get_key { ref( my $self = shift ) or croak 'Oops! Method get_key() is instance, not class.'; my $key = shift; $key = constrain_key( $key ); # Enforce key naming rule. if (exists $self->{$key}) { return $self->{$key}; } else { carp "Oops! Method get_key() reports that \$self->{$key} does not exist."; return undef; } } # Standardize case of hash keys on ucfirst. sub constrain_key { my $key = shift; $key = unquote_neatly( $key ); my @words = split q{ }, $key; for (@words) { $_ = ucfirst $_; } $key = join q{ }, @words; # print "METHOD constrain_key() FILTER: $key \n"; # Debug use only. return $key; } # Whine at user if they ask. sub verbose { ref( my $self = shift ) or croak 'Oops! Method verbose() is instance, not class.'; # Verbosity levels: 0 = Quiet, 1 = Info, 2 = Diagnostic. print $_[0] if $_[1] <= $self->get_key('Verbosity Level'); return 'Pthhht! to Perl::Critic'; } ######################### # CHANNEL (COLUMN) KEYS # ######################### # Allow user to set_col_key data columns such as by: dimension, unit-of-measure, etc. sub set_col_key { ref( my $self = shift ) or croak 'Oops! Method set_col_key() is instance, not class.'; my ($col_key, $value, @cols) = @_; $col_key = constrain_key( $col_key ); for (@cols) { $self->get_key('Column Hrefs')->[$_]->{$col_key} = $value; # print "METHOD set_col_key() SETTING $col_key = $value FOR COLUMN $_ \n"; # Debug use only. } return 'Pthhht! to Perl::Critic'; } # Allow user to qualify data columns such as by: dimension, unit-of-measure, etc. sub get_col_key { ref( my $self = shift ) or croak 'Oops! Method set_col_key() is instance, not class.'; my ($col_key, @cols) = @_; $col_key = constrain_key( $col_key ); my @list; for (@cols) { push @list, $self->get_key('Column Hrefs')->[$_]->{$col_key}; } for (@list) { $_ = 'n/a' unless defined $_ } return @list; } # Describe untidy info about given channels. sub describe { ref( my $self = shift ) or croak 'Oops! Method describe() is instance, not class.'; my $id_string; # Which channels to output? my @chans = ( 0 .. $#{$self->get_key('Column Names')} ); # Default is all. @chans = @_ if scalar @_; # ...unless user provided a list. # NOTE: Excess spaces here cause mis-match at Test.pm on test of # rememberance for column keys. for (@chans) { $id_string .= qq{'Column Order' = $_\n}; $id_string .= q{'Column Name' = } . $self->get_key('Column Names')->[$_] . qq{\n}; while (my ($key, $val) = each %{ $self->get_key('Column Hrefs')->[$_] } ) { $key = quote_neatly($key); $id_string .= "$key = $val\n"; } $id_string .= q{'Data Length' = } . scalar @{ $self->get_key('Column Data Arefs')->[$_] } . qq{\n\n}; } return $id_string; } ########### # CSV I/O # ########### # Winnow first N lines of input file. # Return array of keys for future hash. sub winnow_keys { ref( my $self = shift ) or croak 'Oops! Method winnow_keys() is instance, not class.'; my ( $path_input, ) = @_; my @keys; if ( open my $fh, '<', "$path_input" ) { my $line_1 = <$fh>; chomp $line_1; close $fh; @keys = split m/,\s*/m, $line_1; for (@keys) { $_ = quote_neatly($_) } $self->verbose( "winnow_keys() found:\n\tKEYS: " . join(', ', @keys) . "\n", 2 ); } else { carp "Oops! Problem at get_csv_keys(): $! \n" } return @keys; } # Join as character-delimited according to key in main hash. sub join_data_row { ref( my $self = shift ) or croak 'Oops! Method join_data_row() is instance, not class.'; my $row; $row = join "$self->{'Field Separator'}", @_; return $row . "\n"; } # Print out the the main, actual (tidy) data to CSV sub write_tidy_csv { ref( my $self = shift ) or croak 'Oops! Method write_tidy_csv() is instance, not class.'; my ($path_output, @chans_out) = @_; if ( open my $fh, '>', "$path_output" ) { print {$fh} $self->join_data_row( @{$self->get_key('Column Names')}[@chans_out] ); foreach my $i ( 0 .. $#{ $self->get_key('Column Data Arefs')->[0] } ) { my @data; # Build a row of column cells from selected channel arrays. foreach my $j (@chans_out) { push @data, $self->get_key('Column Data Arefs')->[$j][$i]; } print {$fh} $self->join_data_row( @data ); } close $fh; $self->verbose("Okay, CSV file written to path '$path_output' \n", 1); } else { carp "Oops! Can't open $path_output for writing: $! \n" } } # Print out the untidy, per-column info to CSV. sub write_info_csv { ref( my $self = shift ) or croak 'Oops! Method write_info_csv() is instance, not class.'; my $path_output = shift; $path_output .= '.info'; if ( open my $fh, '>', "$path_output" ) { for ( $self->describe(@_) ) { chomp $_; print {$fh} $_ . qq{\n}; } close $fh; $self->verbose("Okay: CSV file written to path '$path_output' \n", 1); } else { carp "Oops! Can't open $path_output for writing: $! \n" } } # Read in the *.csv.info file of user defs too untidy for CSV. sub read_info_csv { ref( my $self = shift ) or croak 'Oops! Method read_info_csv() is instance, not class.'; my $path_info = shift; my @info; if ( open my $fh, '<', "$path_info" ) { @info = <$fh>; close $fh; $self->verbose("Okay: File *.info read from path '$path_info' \n", 1); } else { carp "Oops! Can't open $path_info for reading: $! \n" } return @info; } # Write out a CSV file... # or both CSV and INFO files. sub write_csv { ref( my $self = shift ) or croak 'Oops! Method write_csv() is instance, not class.'; my $path_output = shift; $self->verbose("write_csv() reports:\n\tPATH: '$path_output' \n", 2); # Which channels to output? my @chans_out = ( 0 .. $#{$self->get_key('Column Names')} ); # Default is all. @chans_out = @_ if scalar @_; # ...unless user provided a list. $self->verbose("\tCHANS: " . join(', ', @chans_out) . "\n", 2); # Write out tidy data to '*.csv' file. $self->write_tidy_csv( $path_output, @chans_out ); # Write out untidy dat to '*.csv.info' file. if ( $self->get_key('Info File Flag') ) { $self->write_info_csv( $path_output, @chans_out ); } return 'Pthhht! to Perl::Critic'; } # Open CSV files munging out select columns into a pair of arrays: one of keys, # one of column arefs. sub merge_csvs { ref( my $self = shift ) or croak 'Oops! Method merge_csvs() is instance, not class.'; my @path_elems = fileparse($_[0], 'csv', 'CSV'); $self->{file_path_base} = $path_elems[1]; # Get columns from CSV files. while (@_) { my $file = shift; my $cols_aref; if (ref $_[0]) { $cols_aref = shift; } else { $cols_aref = []; } $self->verbose( "merge_csvs() reports: \n" . "\tPATH: $file \n" . "\tCOLS: " . join(', ', @$cols_aref) . "\n", 2 ); @path_elems = fileparse($file, 'csv', 'CSV'); $self->{file_path_base} .= $path_elems[0]; $self->{file_path_base} =~ s/\.$/-/m; # Read in the data. $self->get_csv_chans( $file, @$cols_aref ); # Read in untidy '*.csv.info' file. if ( $self->get_key('Info File Flag') ) { $self->get_csv_info( $file, @$cols_aref ); } } $self->{file_path_base} =~ s/-$//m; return 'Pthhht! to Perl::Critic'; } # Read in CSV file, collect two arrays: one of keys one of column arefs. sub read_csv { ref( my $self = shift ) or croak 'Oops! Method read_csv() is instance, not class.'; my ($path_input, @cols) = @_; $path_input = qq|$path_input|; my @keys = $self->winnow_keys( $path_input ); my $parser = Text::CSV::Simple->new; $parser->field_map(@keys); my @hrefs = $parser->read_file( $path_input ); # Array, one href per record my @arefs; foreach my $i ( 0 .. $#keys ) { no warnings; next if $i > 0 && $keys[$i] =~ m/^$ASCII_SQUOT?(T|t)ime/m; push @arefs, []; foreach my $href (@hrefs) { push @{ $arefs[-1] }, $href->{ $keys[$i] }; } } # Keep selected columns only, or all if empty array given as column. @keys = @keys[@cols] if @cols; @arefs = @arefs[@cols] if @cols; # Keep selected columns only. push @{$self->get_key('Column Names')}, @keys; foreach (@arefs) { shift @{$_} } # Sans description text. push @{$self->get_key('Column Data Arefs')}, @arefs; # Give progress info as needed. my $elem_cnt = scalar @{$arefs[0]}; for (1 .. $#arefs) { $elem_cnt .= ', ' . scalar @{$arefs[$_]} } $self->verbose( 'read_csv() extracted these:' . "\n\tKEYS: " . join(', ', @keys) . "\n\tAREFS: " . join(', ', @arefs) . "\n\tELEMS: $elem_cnt \n" , 2 ); return 'Pthhht! to Perl::Critic'; } # Extract channels to existing array from a 2nd file. sub get_csv_chans { ref( my $self = shift ) or croak 'Oops! Method get_csv_chans() is instance, not class.'; my ($file, @cols) = @_; $self->read_csv($file, @cols); croak "Oops! No data from $file at get_csv_chans() sub \n" unless scalar @{$self->get_key('Column Data Arefs')}; return 'Pthhht! to Perl::Critic'; } # Re-assemble untidy column info back into column keys. sub get_csv_info { ref( my $self = shift ) or croak 'Oops! Method get_csv_info() is instance, not class.'; my ($file, @cols) = @_; # Default to all columns if none given. @cols = (0 .. $#{$self->get_key('Column Names')}) unless @cols; my @info = $self->read_info_csv($file . '.info'); my @info_hrefs; for (@info) { chomp $_; if ($_ =~ m/'Column Order'/m) { push @info_hrefs, {} } # New channel. elsif ($_ =~ m/('Column Name'|'Data Length'|^$)/m) { next # Redundant, excess or blank. } else { my ($key, $value) = split / = /, $_; for ($key, $value) { chomp $_; # Lose newline. $_ =~ s/(^\s*|\s*$)//gm # Lose spaces. } # print "METHOD get_csv_info() FOUND: $key = $value \n"; # Debug use only. $info_hrefs[-1]->{$key} = $value; } } # Always for last N columns. $self->info_to_hashes(@info_hrefs[@cols]); return 'Pthhht! to Perl::Critic'; } # Assign recently read-in info as proper column key. sub info_to_hashes { ref( my $self = shift ) or croak 'Oops! Method info_to_hashes() is instance, not class.'; my $i = $#{ $self->get_key('Column Names') }; # Pointer to endmost new column. my @info_hrefs = reverse @_; for ( @info_hrefs ) { while ( my ($key, $value) = each %{$_} ) { # Debug print use only. #print "METHOD info_to_hashes() PASSING $key = $value FOR COLUMN $i TO set_col_key() \n"; $self->set_col_key( $key, $value, $i); } pop @_; --$i; } return 'Pthhht! to Perl::Critic'; } ####################### # TEXT PRETTIFICATION # ####################### # Round off channel data in place. sub round_cols { ref( my $self = shift ) or croak 'Oops! Method round_cols() is instance, not class.'; my $decimals = shift; # Default to all columns if none provided. my @cols = @_; @cols = (0 .. $#{$self->get_key('Column Names')}) unless @cols; for my $i ( @cols ) { for my $j ( 0 .. $#{$self->get_key('Column Data Arefs')->[$i] } ) { $self->get_key('Column Data Arefs')->[$i][$j] = sprintf '%.' . $decimals . 'f', $self->get_key('Column Data Arefs')->[$i][$j]; } } return 'Pthhht! to Perl::Critic'; } # Pad column width of channel to align with column header. sub align_cols { ref( my $self = shift ) or croak 'Oops! Method align_cols() is instance, not class.'; my $widths = shift; my @col_widths; $self->verbose("align_cols() reports:\n", 2); my @key_widths; for (@{$self->get_key('Column Names')}) { $_ =~ s/\s+/ /gm; my $width = length $_; $width += 2 if $_ !~ m/^'.*'$/m; push @key_widths, $width + 1; } $self->verbose("\tKEY WIDTHS: " . join(', ', @key_widths) . "\n", 2); my @max_widths; # Width of each column's widest data element. for ( 0 .. $#key_widths ) { push @max_widths, $self->col_max_width($_) + 1 } $self->verbose("\tMAX WIDTHS: " . join(', ', @max_widths) . "\n", 2); for ( 0 .. $#key_widths ) { $col_widths[$_] = 0 unless $col_widths[$_]; # User-proof sanity check. $col_widths[$_] = $key_widths[$_] if $key_widths[$_] > $col_widths[$_]; $col_widths[$_] = $max_widths[$_] if $max_widths[$_] > $col_widths[$_]; } $self->verbose("\tCOL WIDTHS: " . join(', ', @col_widths) . "\n", 2); # Default to all columns if none provided. my @cols = @_; @cols = (0 .. $#{$self->get_key('Column Names')}) unless @cols; for my $i ( @cols ) { $self->get_key('Column Names')->[$i] = sprintf "%$col_widths[$i]s", quote_neatly( $self->get_key('Column Names')->[$i]); for my $j ( 0 .. $#{$self->get_key('Column Data Arefs')->[0] } ) { $self->get_key('Column Data Arefs')->[$i][$j] = sprintf "%$col_widths[$i]s", $self->get_key('Column Data Arefs')->[$i][$j]; } } return 'Pthhht! to Perl::Critic'; } # For a given column of data, get width of biggest element. sub col_max_width { ref( my $self = shift ) or croak 'Oops! Method col_max_width() is instance, not class.'; my $max = 0; for ( @{$self->get_key('Column Data Arefs')->[$_[0]]} ) { my $width = length $_; $max = $width if $width > $max; } return $max; } ################### # GENERAL SECTION # ################### # Unpad, unquote given string. sub unquote_neatly { my $str = shift; while ( $str =~ m/^($ASCII_SQUOT|\s)/m || $str =~ m/($ASCII_SQUOT|\s)$/m ) { $str =~ s/^($ASCII_SQUOT|\s)//m; $str =~ s/($ASCII_SQUOT|\s)$//m; } return $str; } # Unpad, unquote, requote given string. # This non-OO function is exported. # Could avoid exporting by making pseudo-OO and shifting $self into oblivion. sub quote_neatly { my $str = shift; $str = unquote_neatly( $str ); return qq|'$str'|; } ###################### # STRAIN GAGE MODULE # ###################### # Append columns for a calculated strain gage rectangular rosette. sub sg_set_col_key { ref( my $self = shift ) or croak 'Oops! Method sg_set_col_key() is instance, not class.'; require Text::CSV::Munge::Strain; Text::CSV::Munge::Strain->set_col_key_sg($self, @_); } # Append columns for a calculated strain gage rectangular rosette. sub sg_rosette_rect { ref( my $self = shift ) or croak 'Oops! Method sg_rosette_rect() is instance, not class.'; require Text::CSV::Munge::Strain; Text::CSV::Munge::Strain->rosette_rect($self, @_); } # Append columns for a calculated strain gage delta rosette. sub sg_rosette_delta { ref( my $self = shift ) or croak 'Oops! Method sg_rosette_delta() is instance, not class.'; require Text::CSV::Munge::Strain; Text::CSV::Munge::Strain->rosette_delta($self, @_); } # Reverse calculate three strains from rosette strain values. # Principally used to generate fake data as a module test. # Possibly also of use in rosette-to-gage comparison. sub sg_retro_rosette { my $ignored = shift; # Lose the class. require Text::CSV::Munge::Strain; Text::CSV::Munge::Strain->retro_rosette(@_); } ######### # FINO # ######### 1; __END__ =head1 NAME Text::CSV::Munge.pm B (verb) To modify data in some way the speaker doesn't need to go into right now or cannot describe succinctly. =head1 VERSION Version 0.01 =head1 SYNOPSIS # Init object. my $munged = Text::CSV::Munge->new(); # Verbosity: 0 = Quiet; 1 = Info; 2 = Diagnose. $munged->set_keys( 'Verbosity Level' => 2 ); # Load select columns from plural CSV's. $munged->merge_csvs( 'file_path_1.csv', [], # Empty (or missing) aref defaults to all columns. 'file_path_2.csv', [1], ); # Reduce data from CSV to these many digits per column. $munged->round_cols( 4, 4 ); # Prettify column widths per channel. $munged->align_cols( 8, 8 ); # Write out a copy of munged CVS data. $munged->write_csv( $munged->{file_path_base} . '.csv'); =head1 DESCRIPTION Read in selected columns (channels) from one or more C<*.csv> files and combine them into a array of channel (column) names and an array-of-arrays containing all channel (column) data rows. Primarily aimed at dealing with time-history recordings of data events previously saved in C<*.csv> format. Write out a single C<*.csv> file from the array of channel names and the array-of-arrays containing channel data. Features are deliberately minimalist as other routines for dealing with specific kinds of data are meant to be gathered in other co-related Perl modules. The expected format of all C<*.csv> files is as follows. =over 4 =item First Line The first line shall be a row of names, one for each column (aka channel), quoted and separated by commas, with or without a trailing comma, thus... 'Time (S)','Scan (V)', ...or thus... 'Time (S)', 'Spray (V)' ...or thus... 'Time (S)', 'Spray (V)', 'Time (S)', 'Scan (V)' =item Subsequent Lines All lines (rows) save the first shall contain values separated by commas, again with or without a trailing comma, thus... -.0999752,.2,'987e12',1.234-e5,'Foo','Bar','Any old string', =back =head1 SUBROUTINES/METHODS Most of these you will need to use. Some few you need only know about. =head2 Initialized a new object my $munged = Text::CSV::Munge->new(); =head2 Load select columns form plural CSV files $munged->merge_csvs( $file_1, \@cols_1, $file_2, \@cols_2 ); Read in a succession of CSV files, keeping only such columns from each as desired. Arguments must be give in successive pairs. Each pair must consit of a file path followed by a array reference. The referenced array may contain only integers, these integers representing which columns (0 thru N) from the CSV of the file path shall be retained. If all columns are to be retained for any preceeding filename, reference its columns by an empty array like so... $munged->merge_csvs( $file_1, [], $file_2, \@cols_2 ); ...or else by no array like so... $munged->merge_csvs( $file_1, $file_2, \@cols_2 ); =head2 Store common column info. $munged->set_keys( 'Channel Names' => ['Time', 'Amplitude', 'Current'], 'Units' => ['Time', 'Volts', 'Amps'], ); By I is meant that whatever sort of info this be, all channels store a value for it. No channel may be without it. Common data are tidy data and may thus be written out to a CSV file. The main object (a hash) is used to store info associated with the entire CSV data set. Info stored here will either be solitary scalars or else tidy, equal-length arrays. In either case, consider them as relating to the totality of read-in CSV data for that object. =head2 Return common column info. $munged->get_key('Verbosity Level'); # What it says. Above is the simplest case, a single scalar. @{ $munged->get_key('Column Names') } # List of names for all channels. The next simplest, a simple array, is exampled above. @{ $munged->get_key('Column Data Arefs') }; # List of arefs for all channels. @{ $munged->get_key('Column Data Arefs')->[0] }; # Data array for channel zero. $munged->get_key('Column Data Arefs')->[0]->[0 .. 5]; # List of 1st six points from channel zero. Getting more complex, above are three examples of dealing with an array of arrays. In this case each sub-array contains the data from its respective column...that is to say, that column of data from the 2nd row on down. =head2 Store uncommon column info. $munged->set_col_key($col_key, $value, @cols); By I is meant that whatever sort of info this be, not all channels store a value for it. Not all channels need even have a hash key for it. Unommon data are untidy data and may not be written out to a CSV file. Some other format might store this kind of untidy data into a header separate from the body of data. But untidy data are often needed for calcuation. So each column also has a hash of its own. Here you may store any key/value pair you wish so as to describe the various columns, those anyway which were (for reasons of untidiness) absent from the CSV file but are needful later. Typically, here is where you would store information entered by the user. For example, a Tektronix O'scope will store into a CSV only two columns: time and volts. Other instruments, while storing more columns, will often be equally terse about what their own values represent. Thus the bare CSV file may contain none of the further information needed to interpret or make use of the data. The usuall case with CSV data is to leave interpretation of data to outboard programs (the scripts you write). Those outboard programs will apply, ex-post-facto, all needed formulae required to understand the data. These ex-post-facto formulae will often require variables specific to given columns, such as: "Volts" and "Full Scale" if the column values are "mV/V"; or "Gage Factor" and "Transverse Sensitivity" if the column values are in "microstrain". Column specific variables such as these are typically entered, ex-post-facto, by the user. In your script, store them by means of the C method if they are general, and if no specific sub-module exists to handle that kind of data. A sub-module, however, may exist to handle that very kind of data. There is, for instance, the sub-module C to handle microstrain readings and formulae. Refer to the section on sub modules for more detail. =head2 Return uncommon column info. my @values = $munged->get_col_key($col_key, @cols); Info stored in column-specific hashes is returned as a list beause the request can be made for plural columns. It may be that no such key was set in one or more of the columns which you query. In such case, its place in the list will be held by C<'n/a'> signifying 'not applicable' or 'not appropriate', as you prefer. There being no constraints applied in calling the C method, its use is valid for all column keys regardless of how they were set. So it matters not whether you set the column key by C or by C here in the outer-module or (inappropriately) by a direct call to a method in one of the sub-modules thus, Cset_col_key_foo()>. There being no difference in where or how they are stored, there is only this one retrieval method, here in the outer module. =head2 Describe individual columns print $munged->describe(@cols); Return a multi-line string listing the contents of all key/value pairs which have been set using the C method. If you don't like how it breaks on newlines, simply adjust to suit. For example... my $desc = $munged->describe(@cols); $desc =~ s{\n}{\t}g; # Change newlines into tabs. $desc =~ s{\t\t}{\n}g; # Change former double-newlines to single newlines. =head2 Reduce column resolution $munged->round_cols( digits, @columns ); Reduce the resolution of listed columns by rounding off to a fixed number of decimals. Argument must be an integer followed by those columns to which the method shall be applied. Examples follow... $munged->round_cols( 3, 1, 5, 7 ) # Round off to 3 decimals columns 1, 5 and 7. $munged->round_cols( 1, 0 .. 9 ) # Round off to 1 decimal columns 0 through 9. Why round off digits? Because a final report should contain values of no greater accuracy than the stated value accuracy. It is not proper to state that a value of 1.2459378549567 is accurate to +/- 1% as the value itself exceeds this level of accuracy. =head2 Set column width in characters $munged->align_cols( digits, @columns ); Make columns pretty for text editors by padding to left N (minimum) spaces. Argument must be an integer followed by those columns to which the method shall be applied. Examples follow... Align to a width of 12 characters columns 1, 5 and 7. $munged->align_cols( 12, 1, 5, 7 ) Align to a width of 8 characters columns 0 through 9. $munged->align_cols( 8, 0 .. 9 ) Results will be exceptional if any of the data in given columns are already wider than the integer width given. In such case the colum width will be aligned by the maximum actual data width so that columns indeed shall align. Why align columns? Because a final report should not require a particular viewer to be read. This way your data may be viewed in either a spreadsheet, in a text editor or even in a web browser. The only stipulation is that viewing be done in a monospace font. =head2 Combinational file names print $munged->{file_path_base}; As you open files with the C method, a variable will have kept track of the file names and composed a composite name for output file use. This method will return that composite. =head2 Writing post-munging CSV files $munged->write_csv( '/some/dir/' . $munged->{file_path_base} . '.csv' ); Write an output file of all columns currently retained. Shown is the use of the prior method in auto-creating an output file name. =head2 Enforced hash key naming rules $hash_key_name = constrain_key( $hash_key_name ); Mostly this happens where you can't see it. Just know that if, for whatever reason, you set a hash key (probably for some column-specific quality) to either C it will come out C. Likewise C will be turned into C without your asking for it. This rule is enforced by both the C and C upon storing a value and also by both C and C upon retrieval of same. =head1 CONFIGURATION AND ENVIRONMENT This module requires no configuration. It auto-searches for its dependencies by calling to C. My goal, as always, is OS-independence, but only have recources to design and test on these two platforms only: =over 4 =item NetBSD 2.0.2 running Perl 5.8.7 My main tower unit and both my laptops run this OS. =item WinXP SP2 running ActiveState Perl 5.8.0. At work I am required to endure this laborious, tiresome and decrepit OS. =back =head1 SUB MODULES This outer-module is generalized. Its C method places no constraint upon the name of any column key (column-specific hash key) you wish to set. Thus you may, quite literally, set a column key named "foo" to the value of "bar" for one or more columns. And that is fine. In your own Perl scripts, employ this general C method however you like. Sub-modules, however, are intended to apply to a specific (mostly engineering) field. They will contain formulae which rely upon particular column-specific hash variables having fixed keys. As a user I wished for the need to pay only minimal attention to the existence of sub-modules. I did not wish to bother knowing when to call them, or to keep track of which object belongs to which in the eventual Perl scripts which I shall write to use them. I addressed that by writing into this outer and generalized "parent" module, a mechanism for automatically C-ing the needed sub-module and passing some few arguements to it. Thus there exist the variously named C methods. Each exists to be used in lieu of the more general C method. How it works is exactly the same except for enforcing constraints upon the column key name. Currently bundled with this module are two supporting sub-modules. More are hopefully soon to follow. Possibly, you might care to contribute one for your own specific field? Presently I am envolved with strain readings, hence the first sub-module as will be described next. =head2 Text::CSV::Munge::Strain A sub-module specific to I data used in mechanical engineering. The following medhods are built into this, outer, generalized, parent module so as to handle two-way communication with the specialized sub-module. Except for defering to these, you need not concern yourself over its use, except to refer to its POD, of course. Example use follows... # Provide infor required to calculate rosettes from gages. $munged->sg_set_col_key( 'Ohms', 120, 1 .. 6 ); $munged->sg_set_col_key( 'Gage Factor', 2.01, 1 .. 6 ); $munged->sg_set_col_key( 'Transverse Sensitivity', 0.015, 1 .. 6 ); # Perform some calculations. $munged->sg_rosette_rect( 1 .. 3 ); # Solve 3 gages as a rosette. $munged->sg_rosette_delta( 4 .. 6 ); # Solve 3 gages as a rosette. # Useful to validate results from the two methods above. $munged-sg_retro_rosette( 4 .. 6 ); # Unsolve a rosette as 3 gages. =head2 Text::CSV::Munge::Strain::Test This sub-module is simply a test of this modules functionality, used only when building and/or maintaining this suite of C modules. It has its own POD document detailing its usage. =head1 SEE ALSO This module works hand-in-hand with C in that both keep channel names and channel data in identical kinds of arrays. Thus data gathered by C may be immediately graphed by C without further manipulation. Example follows... my $eps = Chart::EPS_graph->new(600,400); $eps->set( label_top => 'Main Title', label_y1 => 'Left Y Axis Dimension (Units)', label_y1_2 => 'Left Y Axis Label', label_y2 => 'Right Y Axis Dimension (Units)', label_y2_2 => 'Right Y Axis Label', label_x => 'X Axis (Units)', label_x_2 => 'Bottom Main Label', names => $munged->get_key( 'Column Names' ), data => $munged->get_key( 'Column Data Arefs' ), y1 => [7, 8, 10, 11], y2 => [9, 12], ); $eps->write_eps( 'some/dir/graph.eps' ); $eps->display(); Refer to POD in module L> for more complete details. =head1 AUTHOR Gan Uesli Starling > =head1 LICENSE AND COPYRIGHT Copyright (c) 2006 Gan Uesli Starling. All rights reserved. This program is free software; you may redistribute and/or modify it under the same terms as Perl itself. =cut