Public/Get-Weather.ps1

using namespace System.Text

function Get-Weather
{
    [CmdletBinding(DefaultParameterSetName='ZipCodeForecast')]
    [OutputType([string])]
    PARAM (
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName='ZipCodeForecast')]
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName='ZipCodeAlert')]
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName='ZipCodeForecastAlert')]
            [int]$ZipCode,
        [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true,ParameterSetName='GeoForecast')]
        [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true,ParameterSetName='GeoAlert')]
        [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true,ParameterSetName='GeoForecastAlert')]
            [double]$Latitude,
        [Parameter(Mandatory=$true,Position=1,ValueFromPipelineByPropertyName=$true,ParameterSetName='GeoForecast')]
        [Parameter(Mandatory=$true,Position=1,ValueFromPipelineByPropertyName=$true,ParameterSetName='GeoAlert')]
        [Parameter(Mandatory=$true,Position=1,ValueFromPipelineByPropertyName=$true,ParameterSetName='GeoForecastAlert')]
            [double]$Longitude,
        [Parameter(ParameterSetName='ZipCodeForecast')]
        [Parameter(ParameterSetName='GeoForecast')]
        [Parameter(ParameterSetName='ZipCodeForecastAlert')]
        [Parameter(ParameterSetName='GeoForecastAlert')]
            [ValidateSet(0,1,2,3,4,5,6,7,8,9,10,11,12,13)][int]$PeriodsFromNow = 0,
        [Parameter(ParameterSetName='ZipCodeForecast')]
        [Parameter(ParameterSetName='GeoForecast')]
        [Parameter(Mandatory=$true,ParameterSetName='ZipCodeForecastAlert')]
        [Parameter(Mandatory=$true,ParameterSetName='GeoForecastAlert')]
            [ValidateSet('Short', 'Detailed')][string]$ForecastType = 'Short',
        [Parameter(ParameterSetName='ZipCodeForecast')]
        [Parameter(ParameterSetName='GeoForecast')]
        [Parameter(ParameterSetName='ZipCodeForecastAlert')]
        [Parameter(ParameterSetName='GeoForecastAlert')]
            [switch]$IncludeLocation,
        [Parameter(ParameterSetName='ZipCodeForecast')]
        [Parameter(ParameterSetName='GeoForecast')]
        [Parameter(ParameterSetName='ZipCodeForecastAlert')]
        [Parameter(ParameterSetName='GeoForecastAlert')]
            [switch]$IncludeTime,
        [Parameter(Mandatory=$true,ParameterSetName='ZipCodeAlert')]
        [Parameter(Mandatory=$true,ParameterSetName='GeoAlert')]
        [Parameter(Mandatory=$true,ParameterSetName='ZipCodeForecastAlert')]
        [Parameter(Mandatory=$true,ParameterSetName='GeoForecastAlert')]
            [ValidateSet('Short', 'Detailed', 'Full')][string]$AlertType,
        [Parameter(ParameterSetName='ZipCodeAlert')]
        [Parameter(ParameterSetName='GeoAlert')]
        [Parameter(ParameterSetName='ZipCodeForecastAlert')]
        [Parameter(ParameterSetName='GeoForecastAlert')]
            [switch]$IncludeAlertCount,
        [Parameter()]
            [switch]$Speakable
    )

    begin
    {
        $GeoData = $null
        $Abbreviations = @{
            AL = 'Alabama';        AK = 'Alaska';         AZ = 'Arizona'
            AR = 'Arkansas';       CA = 'California';     CO = 'Colorado'
            CT = 'Connecticut';    DE = 'Delaware';       DC = 'DC'
            FL = 'Florida';        GA = 'Georgia';        HI = 'Hawaii'
            ID = 'Idaho';          IL = 'Illinois';       IN = 'Indiana'
            IA = 'Iowa';           KS = 'Kansas';         KY = 'Kentucky'
            LA = 'Louisiana';      ME = 'Maine';          MD = 'Maryland'
            MA = 'Massachusetts';  MI = 'Michigan';       MN = 'Minnesota'
            MS = 'Mississippi';    MO = 'Missouri';       MT = 'Montana'
            NE = 'Nebraska';       NV = 'Nevada';         NH = 'New Hampshire'
            NJ = 'New Jersey';     NM = 'New Mexico';     NY = 'New York'
            NC = 'North Carolina'; ND = 'North Dakota';   OH = 'Ohio'
            OK = 'Oklahoma';       OR = 'Oregon';         PA = 'Pennsylvania'
            RI = 'Rhode Island';   SC = 'South Carolina'; CZ = 'in the Panama Canal Zone'
            TN = 'Tennessee';      TX = 'Texas';          AE = 'in the U.S. Armed Forces - Europe'
            VT = 'Vermont';        VA = 'Virginia';       CM = 'in the Northern Mariana Islands'
            WV = 'West Virginia';  WI = 'Wisconsin';      AA = 'in the U.S. Armed Forces - Americas'
            AS = 'American Samoa'; GU = 'Guam';           MP = 'Northern Mariana Islands'
            PR = 'Puerto Rico';    NB = 'Nebraska';       VI = 'in the U.S. Virgin Islands'
            WY = 'Wyoming';        UT = 'Utah';           AP = 'in the U.S. Armed Forces - Pacific'
            WA = 'Washington';     SD = 'South Dakota';   PI = 'in the Philippine Islands'
            TT = 'in the Trust Territory of the Pacific Islands'
        }

        function Format-NiceDate ([datetime]$Date)
        {
            $Date.ToString('dddd MMMM * a\t h:mm tt') -replace '\*', (Get-OrdinalNumber -Number $Date.Day)
        }

        function Format-AlertHeadline ($Alert)
        {
            [datetime]$Issued = $Alert.sent
            [datetime]$Effective = $Alert.effective
            [datetime]$Ends = if($Alert.ends) { $Alert.ends } else { $Alert.expires }
            [string[]]$Areas = $Alert.areaDesc -split '; '
            [string]$AreaDescription = if($Areas.Count -gt 1) { ($Areas[0..$($Areas.Count-2)] -join ', ') + ' and ' + ($Areas[-1]) } else { $Areas[0] }

            if($Issued -eq $Effective)
            {
                "$($Alert.status) $($Alert.severity) $($Alert.event) issued $(Format-NiceDate $Issued) for $AreaDescription, effective until $(Format-NiceDate $Ends). "
            } else
            {
                "$($Alert.status) $($Alert.severity) $($Alert.event) issued $(Format-NiceDate $Issued) for $AreaDescription, effective from $(Format-NiceDate $Effective) until $(Format-NiceDate $Ends). "
            }
        }
    }

    process
    {
        # Get the $Location table for our $PointURI
        $Location = @{ LAT = $Latitude; LNG = $Longitude }
        if($PSCmdlet.ParameterSetName.StartsWith('ZipCode'))
        {
            if($null -eq $GeoData)
            {
                # Don't import the Zip/GPS mapping until we need it.
                $GeoData = Import-Csv -Path "$PSScriptRoot/../data/zip-geo.csv"
            }
            $Location = $GeoData | Where-Object ZIP -EQ $ZipCode
            if(!$Location)
            {
                throw "Could not find location $ZipCode in the available data."
            }
        }

        [string]$PointUri = "https://api.weather.gov/points/$($Location.LAT),$($Location.LNG)"
        $PointData = Invoke-RestMethod -Uri $PointUri -UseBasicParsing -Headers @{Accept='application/geo+json'}
        [string]$ZoneID = $PointData.properties.forecastZone -replace 'https?://api\.weather\.gov/zones/forecast/'
        # After this statement, assume $Location is a string.
        if($PointData.properties.relativeLocation.properties.city)
        {
            $Location = "$($PointData.properties.relativeLocation.properties.city) $($Abbreviations[$PointData.properties.relativeLocation.properties.state])"
        } elseif($PSCmdlet.ParameterSetName -eq 'ZipCode')
        {
            $Location = "Zip Code $("$ZipCode" -split '' -join ' ')"
        } else
        {
            $Location = "Latitude $Latitude, Longitude $Longitude, "
        }

        $APIResponse = Invoke-RestMethod -Uri "$PointUri/forecast" -UseBasicParsing -Headers @{Accept='application/geo+json'}
        $ForecastData = $APIResponse.properties.periods[$PeriodsFromNow]
        [StringBuilder]$Output = [StringBuilder]::new()
        [bool]$IsForecast = $PSCmdlet.ParameterSetName.Contains('Forecast') -or $PSBoundParameters.ContainsKey('ForecastType')
        [bool]$IsAlert = $PSBoundParameters.ContainsKey('AlertType')

        if($IsForecast)
        {
            if($IncludeLocation)
            {
                if($IncludeTime)
                {
                    [void]$Output.Append("The forecast $($ForecastData.name) for $Location is: ")
                } else
                {
                    [void]$Output.Append("The forecast for $Location is: ")
                }
            } elseif($IncludeTime)
            {
                [void]$Output.Append("The forecast for $($ForecastData.name) is: ")
            }

            if($ForecastType -eq 'Short')
            {
                [void]$Output.Append($ForecastData.shortForecast)
            } else
            {
                [void]$Output.Append($ForecastData.detailedForecast)
            }
            [void]$Output.Append("`n")
        }

        if($IsAlert)
        {
            $APIResponse = Invoke-RestMethod -URI "https://api.weather.gov/alerts/active/zone/$ZoneID"

            if($APIResponse.features.Count -eq 0)
            {
                [void]$Output.Append("There are no current alerts for $($Location -replace ', $').")
                [string]$OutputString = $Output.ToString().Trim()
                if($Speakable)
                {
                    $OutputString = $OutputString -replace '\bwind(s?)\b', 'winnd$1'
                }
                return $OutputString # early-out
            }

            if($IncludeAlertCount)
            {
                [void]$Output.Append("There $(if($APIResponse.features.Count -eq 1) { 'is' } else { 'are' }) $($APIResponse.features.Count) current alerts for $($Location -replace ', $'):`n")
            }

            foreach($Alert in $APIResponse.features)
            {
                $Alert = $Alert.properties
                [string]$Headline = Format-AlertHeadline $Alert
                switch ($AlertType)
                {
                    'Short' {
                        [void]$Output.Append($Headline)
                    }
                    'Detailed' {
                        [void]$Output.Append($Headline)
                        [void]$Output.Append("The description is: ").Append(($Alert.description -replace '^\.\.', '' -replace '\\n', ' ' -replace "`n", ' ' -replace '\* ', ' '))
                        [void]$Output.Append("`n")
                    }
                    'Full' {
                        [void]$Output.Append($Headline)
                        [void]$Output.Append("The description is: ").Append(($Alert.description -replace '^\.\.', '' -replace '\\n', ' ' -replace "`n", ' ' -replace '\* ', ' '))
                        [void]$Output.Append(($Alert.instruction -replace '\\n', ' ' -replace "`n", '' -replace '\* ', ' '))
                        [void]$Output.Append("`n")
                    }
                }
                [void]$Output.Append("`n")
            }
        }

        [string]$OutputString = $Output.ToString().Trim()
        if($Speakable)
        {
            $OutputString = $OutputString -replace '\bwind(s?)\b', 'winnd$1' -replace '\bmph\b', 'miles per hour'
        }
        return $OutputString
    }
}