Saturday, September 16, 2017

It’s always 5 o’clock somewhere: Using .Net and PowerShell’s Extended Type System to find out where

Last Friday, as work-related conversation was slowing down on the PowerShell Slack, someone wondered aloud if it was too early to start drinking. (I assume they were talking about tea.) Someone else answered with the cliché, “It’s always 5 o’clock somewhere,” but these being PowerShell geeks, instead of saying it literally, they said so with a PowerShell script.

That first draft had some bugs and inefficiencies, so I spent too much time tweaking and refining and optimizing it. As Tom Scott can tell you, perfect accuracy when working with time zones and daylight saving time was historically almost impossible in a full application, much less a one-liner, but we have the advantage of being able to rely on functionality built into Windows and .Net.

The goal is to display the areas where it is currently between 5 PM and 6 PM.

The .Net class [System.TimeZoneInfo] has a static method called ::GetSystemTimeZones(). This method queries the registry method of the local computer. It doesn’t just give us a list of the 24+ time zones in the world, it gives us a list of the (as of today) 134 regions with unique time zone offset and daylight saving time rules. As Microsoft has OS’s in each of the 134 regions, they regularly update the list whenever local TZ or DST laws change, keeping it a very accurate information source.

[System.TimeZoneInfo]::GetSystemTimeZones()

But working with the results wasn’t as simple as I thought it should be. A [TimeZoneInfo] object can be used to calculate the current time in a particular time zone, but shouldn’t it be able to do the calculation itself without us having to all the work?

A [TimeZoneInfo] object tells us both the standard name and daylight saving time name for the time zone, and it can be used to calculate whether it is currently in daylight saving time, but, again, shouldn’t it be able to tell us which name is the current name without us having to do the calculation?

We can use PowerShell to add those calculations to the [TimeZoneInfo] class.

When the PowerShell team was first designing PowerShell on top of .Net, .Net was new and still kind of sucked. So they created the Extended Type System, which allowed them to enhance .Net objects. And it allows us to enhance .Net objects. Cool.

So let’s enhance [TimeZoneInfo].

The [datetime] class has a static property ::UTCNow which gives us the current UTC datetime. The [TimeZoneInfo] class has a static method ::ConvertTimeBySystemTimeZoneId() that can convert a datetime to the local time in a given time zone. So let’s define a new script property in [TimeZoneInfo] called .Time which gets the current UTC time and converts it to the local time as defined in the [TimeZoneInfo] object.

We do that using Update-TypeData. The parameters we’re using below are fairly self explanatory. Within a class definition, $This is how we tell an object to look at itself.

Update-TypeData `
    -TypeName   System.TimeZoneInfo `
    -MemberName Time `
    -MemberType ScriptProperty `
    -Value { [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId( [System.DateTime]::UtcNow, $This.Id ) }  `
    -Force

Now let’s define another script property called .Name to give us the appropriate current choice between the value of the .StandardName and the .DaylightName properties.

To do that, we first build an array with the two options, with the .StandardName in position 0 and the .DaylightName in position 1. Then we use the .IsDaylightSavingTime() method of the [TimeZoneInfo] object to tell us if the value in .Time (which we defined above) is in daylight saving time, giving us a $True or $False, which will be dynamically converted to a 1 or a 0, which we use to index into the array and give us the correct option.

Update-TypeData `
    -TypeName System.TimeZoneInfo `
    -MemberName Name `
    -MemberType ScriptProperty `
    -Value { ( $This.StandardName, $This.DaylightName )[($This.IsDaylightSavingTime( $This.Time ))] } `
    -Force

Now when we call ::GetSystemTimeZones(), it gives us all of the information we need, with no further calculation necessary. So we simply filter on those time zones where the hour is 17 (time between 5 PM and 6 PM), and select the desired properties to display.

[System.TimeZoneInfo]::GetSystemTimeZones() |
    Where-Object { $_.Time.Hour -eq 17 } |
    Select-Object -Property Name, DisplayName, Time

Here it is all together.

Update-TypeData `
    -TypeName   System.TimeZoneInfo `
    -MemberName Time `
    -MemberType ScriptProperty `
    -Value { [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId( [System.DateTime]::UtcNow, $This.Id ) } `
    -Force

Update-TypeData `
    -TypeName System.TimeZoneInfo `
    -MemberName Name `
    -MemberType ScriptProperty `
    -Value { ( $This.StandardName, $This.DaylightName )[($This.IsDaylightSavingTime( $This.Time ))] } `
    -Force

[System.TimeZoneInfo]::GetSystemTimeZones() |
    Where-Object { $_.Time.Hour -eq 17 } |
    Select-Object -Property Name, DisplayName, Time

And here are what the results look like when run at 11:01 AM in September in Minnesota in the US.

Name                            DisplayName                                   Time
----                            -----------                                   ----
Morocco Daylight Time           (UTC+00:00) Casablanca                        9/16/2017 5:01:40 PM
GMT Daylight Time               (UTC+00:00) Dublin, Edinburgh, Lisbon, London 9/16/2017 5:01:40 PM
W. Central Africa Standard Time (UTC+01:00) West Central Africa               9/16/2017 5:01:40 PM

No comments:

Post a Comment