
When Laziness is a Virtue - Part 03 of 03
Target Audience: This series is pitched at developers interested in Object Oriented Design Patterns using the Moose framework in Perl.
My implementation consists of three class. Four, if you consider inheritance. The main class is EarnedValue - I call it main as this class exposes the application API. That's the main point.
The two other classes manage the work, divided along the lines of schedule and cost - handling and processing. The class for schedule is Schedule and cost is Cost. Schedule works off the Gantt chart whereas Cost works off the time-log. (Both of these are described in Part 02.)
So where would you expect to find the basic functionality for pulling data out of a spreadsheet? In Cost and Schedule, right?
My implementation actually deposits this functionality in EarnedValue - the main class. Why do I do that? Partly, to avoid duplication of code. I know that both Cost and Schedule require this functionality - so I put it in a third place, and give both these clients hooks into that functionality by passing a reference. So whenever data is to be pulled off a spreadsheet, the client object delegates the fetching operation to the main class.
If you've been following my other presentations in this series, you would think that I need to use Roles, no? Well, that's exactly what I have done here. I haven't used Roles in the purist, didactic sense, of course - which is why you don't see 'Moose::Role' anywhere. Remember, like much of everything else in the Object-oriented way of going about life, Roles are about the concept rather than a specific, narrowly-defined implementation.
The Japanese have a saying "Bolt what you can weld, tape what you can bolt, hold what you can tape." When you see a concept, free yourself from any particular implementation of it. That way, you can do much more. Even though it may not look like you are doing much at all.
So here's the output:
And the code the EarnedValue class that generates it - it's a bunch of code, an eyeful, and please follow it with the handy 10-step guide below :o)
[1.]
First the business, as usual. 'use Moose' and all that. Par for the course. The exception handling is based on object-oriented Exception::Class. Following best-practices (documented in this blog), the $SIG{__DIE__} event-handler is overloaded.
package EarnedValue;
use Moose;
use OLE;
use Win32::OLE::Const "Microsoft Excel";
use File::Spec;
use Moose::Util::TypeConstraints;
use Params::Coerce ();
use Data::Dumper;
use Exception::Class (
'EV::Exception::Base' => {
description => 'Base Exception Class',
},
'EV::Exception::Base::Logger' => {
isa => 'EV::Exception::Base',
description => 'Simply logging!',
},
'EV::Exception::Base::InvalidState' => {
isa => 'EV::Exception::Base',
fields => [ 'EV', 'PV', 'AC'],
description => 'Invalid state',
},
'EV::Exception::Base::MissingData' => {
isa => 'EV::Exception::Base',
fields => [ 'EV', 'PV', 'AC'],
description => 'Incomplete information',
},
);
local $SIG{__DIE__} = sub {
my $err = shift;
if ($err->isa('EV::Exception::Base')) {
die $err;
} elsif ($err->isa('Cost::Exception::Base')) {
die $err;
} elsif ($err->isa('Schedule::Exception::Base')) {
die $err;
} else {
EV::Exception::Base->throw($@);
}
print $@;
};
[2.]
Data - Sources of information: (a.) Spreadsheet_Schedule, (b.) Spreadsheet_Cost, and (c.) activeSpreadsheet.
The results are generated by scraping these two spreadsheets. Naturally, these need to be supplied by the user. Note that a constraint is set on the slot via subtype, to enforce that the user-supplied document exists on disk. If not, exception is thrown.
subtype 'extantFile'
=> as Str
=> where {
( -e $_ );
};
has 'activeSpreadsheet' => (
is => 'rw',
isa => 'extantFile',
predicate => 'has_activeSpreadsheet',
# trigger => \&timeStamped_copy
);
has 'Spreadsheet_Schedule' => (
is => 'rw',
isa => 'extantFile',
predicate => 'has_spreadsheetSchedule',
default => \&evGantt,
);
has 'Spreadsheet_Cost' => (
is => 'rw',
isa => 'extantFile',
predicate => 'has_spreadsheetCost',
default => \&evBasecamp,
);
[3.]
Data - Results-on-demand: EV, PV, AC, SPI, CPI
The meat! These slots hold the results of scraping spreadsheets and other skulldruggery. The mother-lode, if you will. Note that each has attribute 'lazy' set to '1', implementing lazy-loading. The 'default' attribute of each variable is set to a method that - this is how the chain of events is triggered when a value is demanded.
has 'EV' => (
is => 'rw',
isa => 'Num',
default => \&calculateEV,
lazy => 1,
);
has 'PV' => (
is => 'rw',
isa => 'Num',
default => \&calculatePV,
lazy => 1,
);
has 'AC' => (
is => 'rw',
isa => 'Num',
default => \&calculateAC,
lazy => 1,
);
has 'SPI' => (
is => 'rw',
isa => 'Num',
default => \&calculateSPI,
lazy => 1,
);
has 'CPI' => (
is => 'rw',
isa => 'Num',
default => \&calculateCPI,
lazy => 1,
);
[4.]
Objects - MySchedule, MyCost
These are slots for objects encapsulating the requisite functionality for handling and processing time and cost information. Information is parceled into neat intermediate components here for assembly in the final step by main (EarnedValue) class.
has 'MySchedule' => (
is => 'rw',
isa => 'Schedule',
);
has 'MyCost' => (
is => 'rw',
isa => 'Cost',
);
[5.]
Data - Diary
This is an interesting one. It goes with an associated method 'log_Me', which is the exception handler for the application. Any planned exceptions are handled here and a log of key events with time-date stamp is maintained. The slot is for the log-file that 'log_Me' utilizes.
has 'Diary' => (
is => 'rw',
isa => 'Str',
default => \&evLog,
predicate => 'has_Diary',
clearer => 'clear_Diary',
);
[6.]
Data - Other
Note the use of subtype to enforce a constraint. The specification of cell range is constrained by subtype that checks by a regular expression. The range of cells associated with an Excel spreadsheet query is stashed away here for use by querying methods.
subtype 'CellRange'
=> as Str
=> where {
( /^[a-zA-Z]{1,2}\d+$/ || /^[a-zA-Z]{1,2}\d+?:[a-zA-Z]{1,2}\d+$/);
};
has 'Range' => (
is => 'rw',
# isa => 'CellRange',
isa => 'Str',
predicate => 'has_Range',
clearer => 'clear_Range',
);
has 'multiplying_factor' => (
is => 'rw',
isa => 'Num',
default => 1000,
);
[7.]
Methods - calculateEV, calculatePV, calculateAC, calculateSPI, calculateCPI
The high-level methods recruit these middle-level methods. The structure set-up by these methods is critical. As mention in [3.], each method implements a 'default' attribute set upon a lazy variable. What does that mumbo-jumbo mean? Only that the method is fired automatically when the slot is accessed the first time.
Note the exception handling mechanism. The 'log_Me' method is invoked both for logging and handling any exception originating from the invocation of a high-level method.
sub calculateEV {
my $self = shift;
my $EV;
eval {
$EV = $self->MySchedule->tally_EVnumbers->[0] * $self->multiplying_factor;
};
my $e;
if ($e = Schedule::Exception::Base->caught) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
}
return $EV;
};
sub calculatePV {
my $self = shift;
my $PV;
eval {
$PV = $self->MySchedule->tally_PVnumbers->[0] * $self->multiplying_factor;
};
my $e;
if ($e = Schedule::Exception::Base->caught) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
}
return $PV;
};
sub calculateAC {
my $self = shift;
my $AC;
eval {
$AC = $self->MyCost->tally_ACnumbers;
};
my $e;
if ($e = Cost::Exception::Base->caught) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
};
return $AC;
};
sub calculateSPI {
my $self = shift;
my ($netEV,$netPV);
my $SPI;
eval {
$netEV = $self->EV;
$netPV = $self->PV;
};
my $e;
if ( ($e = Cost::Exception::Base->caught)
|| ($e = Schedule::Exception::Base->caught)) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
$SPI = $netEV/$netPV;
};
return $SPI;
};
sub calculateCPI {
my $self = shift;
my ($netEV,$netAC);
my $CPI;
eval {
$netEV = $self->EV;
$netAC = $self->AC;
};
my $e;
if ( ($e = Cost::Exception::Base->caught)
|| ($e = Schedule::Exception::Base->caught)) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
$CPI = $netEV/$netAC;
};
return $CPI;
};
[8.]
Methods - printExecutiveSummary, printCostStructure
The high-level methods - for formatted reporting. The output generated is a report with a very simple structure. It is easy to see how this can be pumped up with all the bling from Google visualization API and other JavaScript/Ajay frameworks.
sub printExecutiveSummary {
my $self = shift;
printf("%10s %12s\n", 'ITEM', 'INR Value');
printf("%10s-%12s\n", '----------', '------------');
printf("%10s %12.2f\n", 'EV', $self->EV);
printf("%10s %12.2f\n", 'PV', $self->PV);
printf("%10s %12.2f\n", 'AC', $self->AC);
printf("%10s-%12s\n", '----------', '------------');
printf("%10s %12.2f\n", 'SPI', $self->SPI);
printf("%10s %12.2f\n", 'CPI', $self->CPI);
printf("%10s-%12s\n", '----------', '------------');
print "\n";
$self->log_Me if $self->has_Diary;
};
sub printCostStructure {
my $self = shift;
my $dashboard;
return unless $self->MyCost->can('ActivityCost');
$dashboard = $self->MyCost->ActivityCost;
printf(" %10s %12s\n", 'ACTIVITY', 'INR Value');
printf(" %10s-%12s\n", '----------', '------------');
my @theItems = sort(keys(%{$dashboard}));
foreach my $thisItem (@theItems) {
printf(" %-10s %12.2f\n", $thisItem, $dashboard->{$thisItem});
};
printf(" %10s-%12s\n", '----------', '------------');
return $dashboard;
}
[9.]
Methods - queryMe, _querySpreadsheet_singleCell, _querySpreadsheet_Range
The are querying methods that use Win32 automation (Win32::OLE) to work with Microsoft Excel data. The wrapper method 'queryMe' is invoked with the value stashed in 'Range' and returns the content as a data-structure (reference). It deploys the internal 'helper' functions (name beginning with an underscore) where the spreadsheet operations actually take place. The wrapper determines what is the appropriate functionality to use based on the arguments passed.
sub queryMe {
# method returns contents of a cell or the cells in a specified range as 2D array.
my $self = shift; my $sheet = shift;
return unless $self->has_Range;
$sheet = 1 unless $sheet;
my $myRange = $self->Range;
if ($myRange =~ /:/ ) {
$self->_querySpreadsheet_Range($myRange, $sheet);
} else {
$self->_querySpreadsheet_singleCell($myRange, $sheet);
};
};
sub _querySpreadsheet_singleCell {
# method returns contents of a single cell.
my $self = shift;
my $cell = shift;
my $sheet = shift;
$sheet = 1 unless $sheet;
my $excel = OLE->CreateObject("Excel.Application");
my $source = $self->activeSpreadsheet;
my $workbook = $excel->Workbooks->Open($source) || die "Unable to open!";
my $evSheet_namu = $workbook->Worksheets($sheet)->{Name};
my $evSheet = $workbook->Worksheets($evSheet_namu);
my $data = $evSheet->Range($cell); my $dataValue = $data->{Value};
# print '| ' . $evSheet_namu . ' | ' . $cell . ' | ' . $dataValue . ' |';
$workbook->Close;
return $dataValue;
};
sub _querySpreadsheet_Range {
# method returns contents of a range of cells (2D array).
my $self = shift;
my $range = shift;
my $sheet = shift;
$sheet = 1 unless $sheet;
my $excel = OLE->CreateObject("Excel.Application");
my $source = $self->activeSpreadsheet;
my $workbook = $excel->Workbooks->Open($source) || die "Unable to open ($source)!";
my $evSheet_namu = $workbook->Worksheets($sheet)->{Name};
my $evSheet = $workbook->Worksheets($evSheet_namu);
my $data = $evSheet->Range($range); my $dataValue = $data->{Value};
# print '| ' . $evSheet_namu . ' | ' . $range . ' |';
$workbook->Close;
return $dataValue;
};
[10.]
Methods - log_Me
This is an Exception Central - uses Exception::Class stack-trace functionality to record logs and (of course) to imprint the stack trace upon the logs when exceptions occur and the code breaks. Did you notice 'EV::Exception::Base::Logger' in Exception::Class? That's what this is about.
sub log_Me {
my $self = shift;
my $e = shift;
my $diary = $self->Diary;
my $eMsg = '';
open(INFO, ">>$diary");
unless ($e) {
print INFO &timeStamp, " OK ";
eval {
EV::Exception::Base::Logger->throw;
};
my $e;
if ($e = EV::Exception::Base::Logger->caught) {
my @lines = ();
foreach my $thisLine (split("\n", $e->trace)) {
push @lines, $thisLine if ($thisLine =~ 'EarnedValue::');
}
print INFO join(' | ', @lines);
} #fi
} elsif ($e->isa('Cost::Exception::Base')) {
$eMsg = Cost::Exception::Base->description . " -> ";
$eMsg .= Cost::Exception::Base::MissingData->description
if $e->isa('Cost::Exception::Base::MissingData');
print INFO &timeStamp, " ERROR ";
print INFO $e->error;
print INFO $e->trace;
print INFO $eMsg;
} elsif ($e->isa('Schedule::Exception::Base')) {
$eMsg = Schedule::Exception::Base->description . " ";
$eMsg .= Schedule::Exception::Base::InvalidState->description
if $e->isa('Schedule::Exception::Base::InvalidState');
print INFO &timeStamp, " ERROR ";
print INFO $e->error;
print INFO $e->trace;
print INFO $eMsg;
}
close INFO;
}
So how did laziness become a virtue in these 10 simple steps? The complete code is provided below, and may also be downloaded from the site. We have seen how all computation is triggered by access - Just In Time (JIT). No computational resources are wasted loading data that is not required. No inventory is maintained, unless required. Let's reinforce this with a scenario:
1. A request for SPI (EarnedValue::SPI)..
2. triggers a JIT request for EV, PV (EarnedValue::EV, EarnedValue::PV) ..
3. setting off Schedule accessors: Schedule::EVnumbers, Schedule::PVnumbers
4. that consume Roles defined in the main EarnedValue class: EarnedValue::queryMe
5. to pull data off spreadsheet into Object slots:
Schedule::EVnumbers, Schedule::PVnumbers
6. for processing into interim results via Schedule methods:
Schedule::tally_EVnumbers, Schedule::tally_PVnumbers
7. which are then stuffed into slots in the main EarnedValue class:
EarnedValue::EV, EarnedValue::PV
8. thus stuffing the slot EarnedValue::SPI
9. and producing the desired result.
The complete class-def for EarnedValue is reproduced below. For Schedule and Cost implementation, please refer the code posted on the Google site (link at the end of page).
Here is what the complete EarnedValue module looks like:
package EarnedValue;
use Moose;
use OLE;
use Win32::OLE::Const "Microsoft Excel";
use File::Spec;
use Moose::Util::TypeConstraints;
use Params::Coerce ();
use Data::Dumper;
use Exception::Class (
'EV::Exception::Base' => {
description => 'Base Exception Class',
},
'EV::Exception::Base::Logger' => {
isa => 'EV::Exception::Base',
description => 'Simply logging!',
},
'EV::Exception::Base::InvalidState' => {
isa => 'EV::Exception::Base',
fields => [ 'EV', 'PV', 'AC'],
description => 'Invalid state',
},
'EV::Exception::Base::MissingData' => {
isa => 'EV::Exception::Base',
fields => [ 'EV', 'PV', 'AC'],
description => 'Incomplete information',
},
);
local $SIG{__DIE__} = sub {
my $err = shift;
if ($err->isa('EV::Exception::Base')) {
die $err;
} elsif ($err->isa('Cost::Exception::Base')) {
die $err;
} elsif ($err->isa('Schedule::Exception::Base')) {
die $err;
} else {
EV::Exception::Base->throw($@);
}
print $@;
};
subtype 'extantFile'
=> as Str
=> where {
( -e $_ );
};
has 'activeSpreadsheet' => (
is => 'rw',
isa => 'extantFile',
predicate => 'has_activeSpreadsheet',
# trigger => \&timeStamped_copy
);
has 'Spreadsheet_Schedule' => (
is => 'rw',
isa => 'extantFile',
predicate => 'has_spreadsheetSchedule',
default => \&evGantt,
);
has 'Spreadsheet_Cost' => (
is => 'rw',
isa => 'extantFile',
predicate => 'has_spreadsheetCost',
default => \&evBasecamp,
);
has 'Diary' => (
is => 'rw',
isa => 'Str',
default => \&evLog,
predicate => 'has_Diary',
clearer => 'clear_Diary',
);
has 'EV' => (
is => 'rw',
isa => 'Num',
default => \&calculateEV,
lazy => 1,
);
has 'PV' => (
is => 'rw',
isa => 'Num',
default => \&calculatePV,
lazy => 1,
);
has 'AC' => (
is => 'rw',
isa => 'Num',
default => \&calculateAC,
lazy => 1,
);
has 'SPI' => (
is => 'rw',
isa => 'Num',
default => \&calculateSPI,
lazy => 1,
);
has 'CPI' => (
is => 'rw',
isa => 'Num',
default => \&calculateCPI,
lazy => 1,
);
has 'MySchedule' => (
is => 'rw',
isa => 'Schedule',
);
has 'MyCost' => (
is => 'rw',
isa => 'Cost',
);
has 'multiplying_factor' => (
is => 'rw',
isa => 'Num',
default => 1000,
);
subtype 'CellRange'
=> as Str
=> where {
( /^[a-zA-Z]{1,2}\d+$/ || /^[a-zA-Z]{1,2}\d+?:[a-zA-Z]{1,2}\d+$/);
};
has 'Range' => (
is => 'rw',
# isa => 'CellRange',
isa => 'Str',
predicate => 'has_Range',
clearer => 'clear_Range',
);
sub calculateEV {
my $self = shift;
my $EV;
eval {
$EV = $self->MySchedule->tally_EVnumbers->[0] * $self->multiplying_factor;
};
my $e;
if ($e = Schedule::Exception::Base->caught) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
}
return $EV;
};
sub calculatePV {
my $self = shift;
my $PV;
eval {
$PV = $self->MySchedule->tally_PVnumbers->[0] * $self->multiplying_factor;
};
my $e;
if ($e = Schedule::Exception::Base->caught) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
}
return $PV;
};
sub calculateAC {
my $self = shift;
my $AC;
eval {
$AC = $self->MyCost->tally_ACnumbers;
};
my $e;
if ($e = Cost::Exception::Base->caught) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
};
return $AC;
};
sub calculateSPI {
my $self = shift;
my ($netEV,$netPV);
my $SPI;
eval {
$netEV = $self->EV;
$netPV = $self->PV;
};
my $e;
if ( ($e = Cost::Exception::Base->caught)
|| ($e = Schedule::Exception::Base->caught)) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
$SPI = $netEV/$netPV;
};
return $SPI;
};
sub calculateCPI {
my $self = shift;
my ($netEV,$netAC);
my $CPI;
eval {
$netEV = $self->EV;
$netAC = $self->AC;
};
my $e;
if (($e = Cost::Exception::Base->caught)
|| ($e = Schedule::Exception::Base->caught)) {
$self->log_Me($e);
} else {
$self->log_Me if $self->has_Diary;
$CPI = $netEV/$netAC;
};
return $CPI;
};
sub printExecutiveSummary {
my $self = shift;
printf("%10s %12s\n", 'ITEM', 'INR Value');
printf("%10s-%12s\n", '----------', '------------');
printf("%10s %12.2f\n", 'EV', $self->EV);
printf("%10s %12.2f\n", 'PV', $self->PV);
printf("%10s %12.2f\n", 'AC', $self->AC);
printf("%10s-%12s\n", '----------', '------------');
printf("%10s %12.2f\n", 'SPI', $self->SPI);
printf("%10s %12.2f\n", 'CPI', $self->CPI);
printf("%10s-%12s\n", '----------', '------------');
print "\n";
$self->log_Me if $self->has_Diary;
};
sub printCostStructure {
my $self = shift;
my $dashboard;
return unless $self->MyCost->can('ActivityCost');
$dashboard = $self->MyCost->ActivityCost;
printf(" %10s %12s\n", 'ACTIVITY', 'INR Value');
printf(" %10s-%12s\n", '----------', '------------');
my @theItems = sort(keys(%{$dashboard}));
foreach my $thisItem (@theItems) {
printf(" %-10s %12.2f\n", $thisItem, $dashboard->{$thisItem});
};
printf(" %10s-%12s\n", '----------', '------------');
return $dashboard;
}
sub evGantt {
my $folder = File::Spec->curdir();
my $file = q{Dashboard.xls};
return File::Spec->rel2abs(File::Spec->catfile($folder, $file));
};
sub evBasecamp {
my $folder = File::Spec->curdir();
my $file = q{time-export.csv};
return File::Spec->rel2abs(File::Spec->catfile($folder, $file));
}
sub evLog {
my $folder = File::Spec->curdir();
my $file = q{main.log};
return File::Spec->rel2abs(File::Spec->catfile($folder, $file));
};
sub timeStamped_copy {
# for safety, make a time-stamped copy before the spreadsheet is accessed.
my $self = shift;
};
sub queryMe {
# method returns contents of a cell or the cells in a specified range as 2D array.
my $self = shift; my $sheet = shift;
return unless $self->has_Range;
$sheet = 1 unless $sheet;
my $myRange = $self->Range;
if ($myRange =~ /:/ ) {
$self->_querySpreadsheet_Range($myRange, $sheet);
} else {
$self->_querySpreadsheet_singleCell($myRange, $sheet);
};
};
sub _querySpreadsheet_singleCell {
# method returns contents of a single cell.
my $self = shift;
my $cell = shift;
my $sheet = shift;
$sheet = 1 unless $sheet;
my $excel = OLE->CreateObject("Excel.Application");
my $source = $self->activeSpreadsheet;
my $workbook = $excel->Workbooks->Open($source) || die "Unable to open!";
my $evSheet_namu = $workbook->Worksheets($sheet)->{Name};
my $evSheet = $workbook->Worksheets($evSheet_namu);
my $data = $evSheet->Range($cell); my $dataValue = $data->{Value};
# print '| ' . $evSheet_namu . ' | ' . $cell . ' | ' . $dataValue . ' |';
$workbook->Close;
return $dataValue;
};
sub _querySpreadsheet_Range {
# method returns contents of a range of cells (2D array).
my $self = shift;
my $range = shift;
my $sheet = shift;
$sheet = 1 unless $sheet;
my $excel = OLE->CreateObject("Excel.Application");
my $source = $self->activeSpreadsheet;
my $workbook = $excel->Workbooks->Open($source) || die "Unable to open ($source)!";
my $evSheet_namu = $workbook->Worksheets($sheet)->{Name};
my $evSheet = $workbook->Worksheets($evSheet_namu);
my $data = $evSheet->Range($range); my $dataValue = $data->{Value};
# print '| ' . $evSheet_namu . ' | ' . $range . ' |';
$workbook->Close;
return $dataValue;
};
sub log_Me {
my $self = shift;
my $e = shift;
my $diary = $self->Diary;
my $eMsg = '';
open(INFO, ">>$diary");
unless ($e) {
print INFO &timeStamp, " OK ";
eval {
EV::Exception::Base::Logger->throw;
};
my $e;
if ($e = EV::Exception::Base::Logger->caught) {
my @lines = ();
foreach my $thisLine (split("\n", $e->trace)) {
push @lines, $thisLine if ($thisLine =~ 'EarnedValue::');
}
print INFO join(' | ', @lines);
} #fi
} elsif ($e->isa('Cost::Exception::Base')) {
$eMsg = Cost::Exception::Base->description . " -> ";
$eMsg .= Cost::Exception::Base::MissingData->description
if $e->isa('Cost::Exception::Base::MissingData');
print INFO &timeStamp, " ERROR ";
print INFO $e->error;
print INFO $e->trace;
print INFO $eMsg;
} elsif ($e->isa('Schedule::Exception::Base')) {
$eMsg = Schedule::Exception::Base->description . " ";
$eMsg .= Schedule::Exception::Base::InvalidState->description
if $e->isa('Schedule::Exception::Base::InvalidState');
print INFO &timeStamp, " ERROR ";
print INFO $e->error;
print INFO $e->trace;
print INFO $eMsg;
}
close INFO;
}
sub timeStamp {
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst);
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
my $tstamp = sprintf
"\[%02d\-%02d\-%4d : %02d\-%02d\-%02d\]",
mon+1,$mday,$year+1900,$hour,$min,$sec;
return $tstamp;
}
All code in this series may be downloaded from:
http://sites.google.com/site/sanjaybhatikar/codeunquote/designpatterns-1
http://sites.google.com/site/sanjaybhatikar/codeunquote/designpatterns-1
Code has been uploaded to the Google site.
ReplyDelete