#!/usr/bin/perl use strict; use warnings; use POSIX qw/strftime/; use Time::Local qw/timelocal/; my $FORECAST_FILENAME = 'wx-combined.txt'; my $ACTUALS_FILENAME = 'temps-combined.txt'; my $TWELVE_HOURS = 12 * 60 * 60; # Main ========================================================================= chomp(my @forecasts = read_file($FORECAST_FILENAME)); chomp(my @observations = read_file($ACTUALS_FILENAME)); my $actuals_ref = analyze_observations(@observations); my @reports = analyze_forecasts(\@forecasts, $actuals_ref); print_report(@reports); 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. # Also, the 'obs' key in the inner hash 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 $timestamp_of_record = $timestamp - $TWELVE_HOURS; my $date_of_record = strftime('%Y-%m-%d', localtime($timestamp_of_record)); 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 # (as a single string) for each one. The report may indicate that not enough # actuals data is available, or else describes and compares the forecast and # actuals; most of the latter is handled by the report_temperatures subroutine. # Returns a list with one report string per element in sorted order by date. sub analyze_forecasts { my ($forecasts_ref, $actuals_ref) = @_; my @reports; foreach my $forecast (sort @{$forecasts_ref}) { my ($date, $time, $forecast_highs, $forecast_lows) = split(/\t/, $forecast); unless (exists $actuals_ref->{$date}) { push(@reports, "Skipped $date: No actuals found\n"); next; } if ($actuals_ref->{$date}{'obs'} < 24) { push(@reports, "Skipped $date: Only $actuals_ref->{$date}{'obs'} hours of data\n"); next; } my $report = 'On '; my ($year, $month, $day) = split(/-/, $date); $report .= strftime('%B %e', localtime(timelocal(0, 0, 0, $day, $month - 1, $year))); $report .= ', '; $report .= report_temperatures('high', $forecast_highs, $actuals_ref->{$date}{'max'}); $report .= '; '; $report .= report_temperatures('low', $forecast_lows, $actuals_ref->{$date}{'min'}); $report .= ".\n"; push(@reports, $report); } return @reports; } # report_temperatures(high/low, forecast, actual) => report fragment # # For a day, takes a forecast type ('high' or 'low'), the forecast string (e.g., # "UPPER 80S"), and the actual corresponding high or low temperature, and builds # the part of the report that describes the forecast, actual, and the accuracy # of the forecast. For example: # # ('high', 'UPPER 80S', 85.0) => # 'the high was forecast to be in the upper 80s and the actual high was 85.6F, 1.4F lower than predicted' # # ('low', 'AROUND 70', 69.5) => # 'the low was forecast to be around 70 and the actual low was 69.5F, as predicted' # # Returns the report fragment string. sub report_temperatures { my ($hilo, $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); } # print_report(report elements) # # Prints the final report, combining and formatting each of the daily reports in # the given list. No return value. sub print_report { my @reports = @_; # WRITE THIS SUBROUTINE! }