|A short Windows PowerShell script makes it easy to convert time measured in days, hours, minutes, and seconds into more easily compared time-span values using the Microsoft .NET Framework TimeSpan structure.|
There's no real standard for how applications should display or log information about the duration of activities. Some applications might show these durations or time spans in a human-understandable form, showing some combination of days, hours, minutes, seconds, and milliseconds. Others stick to a single unit such as seconds or days and use fractional quantities where necessary. Depending on how an application represents a time span, you might have to do some work to make the information usable elsewhere, or sometimes even to get a sense of how long the time span is. The To-TimeSpan.ps1 script and some other techniques I'll demonstrate in this article make universal time-span translation possible using Windows PowerShell. Let's talk about what a time span actually is, then look at how to use the script to simplify translation and some examples of its usage for real-world activities.
Time Spans: Measures of Elapsed Time
A time span is a special quantity because it's a measure of elapsed time between two events, as opposed to a distinct point in time at which something takes place. Time spans are important because most activities we engage in are repetitive, and knowing how often activities occur and how long they take to happen is an intrinsic part of how we think about them. We think about durations so often that we aren't even aware of them. We ask questions such as, How long does it take your backup to run? How long should you microwave the popcorn? How long does it take you to fill out your timesheet each day? What's the Ping response time for server x? How long does it take for Windows to boot up?
We don't usually measure all those things in detail, but when it's important to do so—for example, when you're trying to determine whether your backup system is still backing up as quickly as it used to—our system of representing time spans makes it difficult to compare times easily. Your logs might show that a backup took 8 hours, 26 minutes, and 58 seconds to back up 102.2GB of data on one day. A week later, after some file cleanup and a couple of changes to the backup software settings, a backup takes 8 hours, 19 minutes, and 14 seconds to back up 100.9GB. The backup takes less time, but the amount of data backed up is smaller, so how can you tell whether the changes make the backup run faster? You can calculate the answer by converting numbers into a common measure such as seconds or days, then dividing the backup size by the elapsed time for each backup—but doing so takes a bit of work with a calculator for most people.
The Microsoft .NET Framework runtime has a System.TimeSpan class designed specifically to support time-span calculations. If you give a System.TimeSpan object a time measured in terms of days, minutes, hours, seconds, and milliseconds, it will convert them into a single duration. This duration is measured in terms of ticks (100 nanosecond intervals) by default, but you can also extract it as a total value in whatever measurement you want: days, minutes, seconds, and so on. The To-TimeSpan script uses the System.TimeSpan structure to handle calculation work.
I wrote To-TimeSpan specifically to make it easy to enter mixed-unit time spans. For that reason, To-TimeSpan uses a single argument, the time span in days:hours:minutes:seconds form. You don't have to have all the values, but the script assumes the smallest portion of the time-span data is in seconds, so you do need to provide colons (:) to indicate lower values that are missing. For example, you can convert 8 days and 2 minutes into a time span like this:
Because the script automatically treats an empty value like 0, you can simplify it to this instead:
Since minutes and seconds are the lowest values, you can specify 16 minutes and 35 seconds even more simply:
You can also use negative values if you want. For example,
which is like saying "1 day minus 1 second," returns the same TimeSpan as
Though you don't need to enter milliseconds as part of the time span, you can do so. However, you must enter them as a separate argument following the initial time span. The span of time 2 minutes, 3.5 seconds can also be described as 2 minutes, 3 seconds, and 500 milliseconds, so you'd specify the value in this way:
To-TimeSpan 2:3 500
Using To-TimeSpan in Simple Calculations
The To-TimeSpan script returns what looks like a pretty complex object. Let's look at the backup example I mentioned earlier. If we use the 8 hours, 26 minutes, and 35 seconds duration to generate a time span like this
PowerShell will show the following data on screen:
Days : 0 Hours : 8 Minutes : 26 Seconds : 35 Milliseconds : 0 Ticks : 303950000000 TotalDays : 0.351793981481481 TotalHours : 8.44305555555555 TotalMinutes : 506.583333333333 TotalSeconds : 30395 TotalMilliseconds : 30395000
I could have written the To-TimeSpan script to return a single type of value, but doing so would have made the script less flexible. You can make your own choice about how to understand the TimeSpan by using one of the Total* properties. To get the TimeSpan's TotalHours, for example, you can use this code:
which returns the value 8.44305555555555. (For more information about the TimeSpan structure, see Microsoft's System.TimeSpan documentation at msdn2.microsoft.com/en-us/library/system.timespan_members.aspx.) Alternatively, to view the TimeSpan members, you can generate a time-span instance and pass it into the PowerShell Get-Member cmdlet's output, like this:
To-TimeSpan 8:26:35 | Get-Member
Or, because a TimeSpan object is a simple .NET object already available in PowerShell, you can even create a TimeSpan object with an arbitrary duration such as 0 and pass it to Get-Member:
\[TimeSpan\]0 | Get-Member
So how fast is the backup running on each occasion, measured in gigabytes per hour? We've reduced it to a simple division problem, which PowerShell can work out for us. The commands and their results are:
102.2/(To-TimeSpan 8:26:35).TotalHours 12.1046224708011 101.9/(To-TimeSpan 8:19:14).TotalHours 12.2467783935368
As you can see from these results, the backup system actually is backing up more data per hour than it was previously.
How To-TimeSpan Works
The To-TimeSpan script is very compact. We'll walk through it because if you aren't familiar with how PowerShell works, it won't be intuitively obvious how I did some things. You don't need to understand the internals to use the script, however.
The param statement at callout A in Listing 1 simply tells PowerShell that we expect a string value as an argument and that it will be called $Timespan within the script. If and only if no command-line argument is provided, PowerShell will use the default value after the equal (=) sign. Instead of providing a specific default value, however, I tell PowerShell to run the Read-Host cmdlet with the prompting text "Enter a time span in \[\[\[days:\]hours:\]minutes:\]seconds format". This prompt gives you a useful hint about what the script expects if you don't provide the argument. You can also cancel execution at this point by pressing Ctrl+C. If you press Enter without providing a value, the script will treat the empty value as 0 and return a System.TimeSpan object with 0 duration.
There are various ways to convert the easily entered $Timespan string into something that can construct a time span. I use the most general way, which is organizing the data as an array of days, hours, minutes, seconds, and milliseconds. The rest of the script deals with getting the data into this form.
The code at callout B turns the data into an array of elements by splitting it at the colons and stores the array in the variable $values. Although the $Timespan.Split(":") expression by itself returns an array, problems would occur if we stopped there. First, if you submit only a single element such as seconds, PowerShell would automatically unwrap the array and return it as a single value. Second, generic .NET arrays don't let you insert values at the start of the array. To do so, you need a specialized array type: .NET's System.Collections.ArrayList. (You can find documentation for the ArrayList type at msdn2.microsoft.com/en-us/library/system.collections.arraylist_members.aspx.) The \[System.Collections.ArrayList\] line coerces the result of our split into being an ArrayList.
The code at callout C inserts leading zeros to pad the array out into days,hours,minutes,seconds. This padding step is what ensures that 16:35 is treated as 16 minutes and 35 seconds; without it, we would have needed to specify the time span as 0:0:16:35 instead. To insert the leading zeros, we use the ArrayList's Insert method, which takes two arguments. The first is the index of the position at which an element is inserted, and the second is the value of the element.
This brings us to another question: How do I know that an ArrayList has the Insert method? If you try creating an ArrayList and checking its members by piping it into the Get-Member cmdlet—the usual way to check object methods in PowerShell—you find that the ArrayList gets unwrapped. For example, if you try to use Get-Member on an ArrayList containing a couple of arbitrary numbers as elements, like this:
\[System.Collections.ArrayList\]@(7,42) | get-member
You'll get members for the data type System.Int32, not for the ArrayList. If you try to use an empty ArrayList, like this:
\[System.Collections.ArrayList\]@() | get-member
PowerShell gives you the error Get-Member : No object has been specified to get-member.
This is one of the places where the PowerShell's helpful automatic array unwrapping becomes a hindrance. We want to look at the ArrayList itself. PowerShell unwraps the ArrayList and passes each item in the ArrayList into Get-Member. So with a couple of integer values in the ArrayList, Get-Member actually returns members of integers. In the second case, where the ArrayList was empty, PowerShell passed nothing at all into Get-Member, causing the "no object" error. You can avoid this unwrapping if you give Get-Member the object of interest as a value for its InputObject parameter. You can view the ArrayList methods by using this statement:
Get-Member -InputObject (\[System.Collections.ArrayList\]@())
Note the parentheses () surrounding our empty ArrayList. The parentheses let PowerShell know that the contents are an expression rather than a literal value.
The code at callout D handles the optional milliseconds you might have specified. Since the ArrayList has four values with indices 0 through 3, we insert the milliseconds value at index 4. This value defaults to 0 if you haven't specified milliseconds.
The last step is to create the System.TimeSpan object at callout E. The New-Object cmdlet in PowerShell is analogous to CreateObject in VBScript. The cmdlet takes a single required argument: the known name of a particular structure to create an object from. Since you can use arguments to construct .NET objects instead of having to fill in values later, New-Object optionally takes arguments for the object. If we gave To-TimeSpan the initial argument 8:26:35, the code at callout E is equivalent to this statement:
New-Object System.TimeSpan 0,8,26,35,0
If you're more used to scripting environments such as PowerShell, it might seem odd to you that we don't explicitly return this object. That's because we don't need to. Since PowerShell is a shell scripting language, it automatically assumes that the results of any expressions it evaluates are meant to be used. If you don't put them into a variable or throw them away, PowerShell will send them on to the output stream.
Time Spans Simplified
You now have an easy-to-use method for translating dissimilar time-span values, which can give you a more meaningful understanding of the durations of various activities. Let me know what uses you find for this practical script!