#!/usr/bin/perl use strict; use warnings; use File::Basename qw/basename/; use Getopt::Long; use POSIX qw/strftime/; use Text::Wrap; use Time::Local qw/timelocal/; my $FORECAST_FILENAME = 'homework-14-forecasts.txt'; my $ACTUALS_FILENAME = 'homework-14-actuals.txt'; my $TWELVE_HOURS = 12 * 60 * 60; my $below_count = 0; my $above_count = 0; my $good_count = 0; # Main ========================================================================= GetOptions('test' => \&run_tests); chomp(my @observations = read_file($ACTUALS_FILENAME)); my $actuals_ref = analyze_observations(@observations); chomp(my @forecasts = read_file($FORECAST_FILENAME)); print analyze_forecasts(\@forecasts, $actuals_ref); print analyze_counts(64); exit(0); # Subroutines ================================================================== # read_file(filename) # # Reads the contents of the given filename and returns them as a single scalar # or as a list of lines, depending on context. Dies upon failure. sub read_file { my $filename = shift; # Read file contents into array open(my $filehandle, '<', $filename) or die "Could not open '$filename': $!\n"; my @lines = <$filehandle>; close($filehandle); # Choose the correct return format return @lines if wantarray; return join('', @lines) if defined wantarray; return; } # analyze_observations(hourly observations) => hash # # Processes each HOURLY line from the observations file and builds a simple data # structure of DAILY actual highs and lows. Returns a reference to a hash that # contains data like this: # # { # '2011-07-29' => { 'max' => 86.500, 'min' => 71.213, 'obs' => 24 }, # '2011-07-30' => { 'max' => 88.115, 'min' => 72.102, 'obs' => 24 }, # ... # } # # Notes: The definition of a "day" is unusual: The observations for a particular # date run from noon on that day through the 11 a.m. hour of the following day. # Thus, the 'min' key in the inner hash is most likely the minimum temperature # of the following day. Also, the 'obs' key is the number of observations # counted for that day; 24 are required for a complete record of a day. sub analyze_observations { my %actuals; foreach my $hourly_observation (@_) { my ($date, $hour, $min, $max) = split(/\t/, $hourly_observation); my ($year, $month, $day) = split(/-/, $date); my $timestamp = timelocal(0, 30, $hour, $day, $month - 1, $year); my $date_of_record = strftime('%Y-%m-%d', localtime($timestamp - $TWELVE_HOURS)); if (exists $actuals{$date_of_record}) { $actuals{$date_of_record}{'min'} = $min if $min < $actuals{$date_of_record}{'min'}; $actuals{$date_of_record}{'max'} = $max if $max > $actuals{$date_of_record}{'max'}; $actuals{$date_of_record}{'obs'} += 1; } else { $actuals{$date_of_record} = {'min' => $min, 'max' => $max, 'obs' => 1}; } } return \%actuals; } # analyze_forecasts(forecast data, actuals data) => list # # Main analysis subroutine: Goes through the forecast days and builds a report # for each one. The report may indicate that not enough observation data is # available, or else describes and compares the forecast and actuals; most of # the latter is handled by a separate subroutine. Returns a string containing # the entire report. sub analyze_forecasts { my ($forecasts_ref, $actuals_ref) = @_; my $report = "WEATHER REPORT\n\n"; $report .= "DATE HIGHS LOWS (early next AM)\n"; $report .= "================ ===================== =====================\n"; foreach my $forecast (sort @{$forecasts_ref}) { my ($date, $time, $forecast_highs, $forecast_lows) = split(/\t/, $forecast); # Date my ($year, $month, $day) = split('-', $date); my $weekday = substr(strftime('%A', localtime(timelocal(0, 0, 12, $day, $month - 1, $year - 1900))), 0, 3); $report .= "$date ($weekday) "; # Check for observations unless (exists $actuals_ref->{$date}) { $report .= "---------- no weather observations ----------\n"; next; } if ($actuals_ref->{$date}{'obs'} < 24) { $report .= "-------- only $actuals_ref->{$date}{'obs'} weather observations --------\n"; next; } $report .= format_cell($forecast_highs, $actuals_ref->{$date}{'max'}); $report .= ' '; $report .= format_cell($forecast_lows, $actuals_ref->{$date}{'min'}); $report .= "\n"; } return $report; } # format_cell(forecast, actual) => report fragment # # For a day, takes the forecast string (e.g., "UPPER 80S"), and the actual # corresponding high or low temperature, and builds the table cell that # describes the forecast, actual, and the accuracy of the forecast. # # Also, this subroutine increments the global counts for forecast accuracy -- # $below_count, $above_count, and $good_count -- as it compares forecasts and # actual observations. # # Returns the report fragment string. Example return values: # # format_cell('UPPER 80S', 85.021) => '87-89 => 85.0 (below)' # format_cell('AROUND 70', 69.557) => '69-71 => 69.6 (good) ' sub format_cell { my ($forecast, $actual) = @_; my ($forecast_min, $forecast_max) = compute_forecast_range($forecast); # WRITE THIS SUBROUTINE! return $report; } # compute_forecast_range(forecast) => (minimum, maximum) # # Takes a string forecast (e.g., "UPPER 80S") and determines the actual forecast # temperature range; returns the minimum and maximum temperatures of the range. # Examples: # # AROUND 80 => (79, 81) # LOWER 80S => (81, 83) # MID 80S => (84, 86) # UPPER 80S => (87, 89) sub compute_forecast_range { my $forecast_string = shift; # WRITE THIS SUBROUTINE! return ($minimum_temperature, $maximum_temperature); } # analyze_counts() => report # # Analyzes the counts of forecast accuracy and returns a multiline string that # contains a textual report on the counts. The result string is wrapped to the # given column width. # # Example return value (wrapped to 64 columns): # # Of the 8 forecasts, the actual temperatures were below the # forecast 3 times, above the forecast 1 time, and accurate 4 # times. Overall accuracy was 50%. sub analyze_counts { my $column = shift; my $report = ''; # WRITE THIS SUBROUTINE! return $report; } # Tests ======================================================================== sub run_tests { require Test::More; Test::More->import; plan(tests => 2); is((compute_forecast_range("UPPER 80S"))[0], 87, 'upper 80s minimum'); is((compute_forecast_range("UPPER 80S"))[1], 89, 'upper 80s maximum'); exit(0); }