Top of the Next Hour

The Get-Date cmdlet has always been helpful, but just when I thought it had me fully covered, I determined it fell short. That said, it does have enough to get me what I want, even if there isn't a simple, single built-in method for it.

I'm working on a project that requires me to add an additional trigger to a previously created scheduled task. When the scheduled task was initially deployed, it only had a single trigger. It was to run at midnight the following day (from the day in which the task was first created), and then every hour forever (at the top of the hour), until the end of time. Well, for the next 10,675,199 days at least.*

As a part of updating this project with a new trigger, I'm also going to be overwriting the trigger that begins at midnight. I'm doing that now, because at this point in this project, I don't want to wait until tomorrow morning (at midnight) to have the task up and running again, I need a better starting time now that the task has been operating successfully for months. While I can do an hour from now -- whatever time that may be -- for the New-ScheduledTaskTrigger's At parameter...

PS > (Get-Date).AddHours(1)

Saturday, January 5, 2019 8:14:29 PM

I don't want that.

What this would mean, is that my fleet of AWS EC2 instances would all have random times in which they execute this task, dependent on when the task was updated. To better complete this picture, this task downloads a PowerShell module from AWS S3 and plops it on the EC2 instance running the task. Historically, or currently rather, I've greatly appreciated knowing that any modifications to the PowerShell module uploaded to S3, are downloaded to all the EC2 instances, at the top of any, and every, hour. At 10:00 a.m., the module is replaced. At 11:00 a.m., the module is replaced, and so on. A collection of random, unknown times would be horrible going forward. I need to know that any modifications to the PowerShell module in S3 will be on all the instances, every hour, at the same time. And even if there's no modifications to the module, it gets downloaded anyway. It's just easier that way (for now, perhaps).

Therefore, I need to know the top of the next hour. You know, if it's 7:16 p.m., I need Get-Date to return 8:00 p.m. (on the same day, of course). If it 4:30 a.m, I need 5:00 a.m. returned. While I didn't find a built-in method to accomplish this, as stated, I was able to write something myself, after a short amount of time head down in the console. Take a look at the below commands, and then let's discuss them.

PS > Get-Date

Saturday, January 5, 2019 7:19:17 PM

PS > (Get-Date).AddMinutes(59 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)

Saturday, January 5, 2019 8:00:00 PM

The first above example returns the current date and time, as we'd expect that it would. The second above example indicates how we can ensure the value returned by Get-Date is the same date as today -- no changes wanted there -- with the time set in the future, at the top of the next hour.

For fun, we'll pretend the time is 10:43:19 a.m.

Here's what would happen, if the second command ran against this time. First, it would use Get-Date's AddMintues method. That makes sense, as no matter what time it is, we'll need to add time to the current time, to get to the top of the next hour. Therefore, within the AddMinutes method, we take 59 and subtract the current minute of the current time.

10:43:19

59 - 43 = 16 minutes

Next, we'd add some seconds to our time, as well. We would take the value of 60 and subtract the current second of the current time.

10:43:19

60 - 19 = 41 seconds

Adding 41 seconds to 10:43:19 makes it 10:44:00. Adding in those 16 minutes, takes us right to 11:00:00. If you didn't catch it, the command uses 59, not 60, when calculating AddMinutes. This is because the AddSeconds method is going to make up our "missing" minute.

Take a look at the following two examples. I won't bother to explain them, but perhaps at this point, you can understand why they produce the results they do.

PS > (Get-Date).AddMinutes(60 - (Get-Date).Minute).AddSeconds(59 - (Get-Date).Second)

Saturday, January 5, 2019 8:00:59 PM

PS > (Get-Date).AddMinutes(60 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)

Saturday, January 5, 2019 8:01:00 PM

Now, no matter when my instances have their trigger updated, for the same task across each of them, I can ensure this task is back to updating my PowerShell module on those instances, at the top of every hour.

I took this one step further, and not necessarily because it had anything to do with scheduled tasks. What if I wanted my own method to do this? I quickly wrote a ScriptMethod for my own instance of a Get-Date object. I don't have an opportunity, or need to do this often, so every little bit of practice is helpful.

$Date = Get-Date
 
Add-Member -InputObject $Date -MemberType ScriptMethod -Name GetNextTopHour -Value {
    $this.AddMinutes(59 - (Get-Date).Minute).AddSeconds(60 - (Get-Date).Second)
}
 
$Date = $Date.GetNextTopHour()

Now we can use our datetime object, and return the top of the next hour.

PS > $Date

Saturday, January 5, 2019 8:00:00

Now back to thinking though the task that started this whole line of thought, anyway.

* For anyone curious, when the scheduled task's, task trigger was originally created using the New-ScheduledTaskTrigger function, the value used for the RepetitionDuration parameter was set as [System.TimeSpan]::MaxValue. Take a look at the below example, and you'll see where this 10 million plus day count is derived.

PS > [System.TimeSpan]::MaxValue
Days              : 10675199
Hours             : 2
Minutes           : 48
Seconds           : 5
Milliseconds      : 477
Ticks             : 9223372036854775807
TotalDays         : 10675199.1167301
TotalHours        : 256204778.801522
TotalMinutes      : 15372286728.0913
TotalSeconds      : 922337203685.478
TotalMilliseconds : 922337203685477

In order that we're all on the same page, those 10 million plus days, equate to "indefinitely" in a scheduled task's Triggers tab, when viewing the task in the GUI. If you're ever after a scheduled task repetition that never ends, and you're using PowerShell to piece your task together, then MaxValue is the property to use.

≥ Tommy Maynard (Twitter: @thetommymaynard)

5 thoughts on “Top of the Next Hour

    1. Tommy Maynard (tommymaynard) Post author

      Hi Shane -- thank you for your reply. I don't think there's anything wrong with your example, other than you came up with it first. It absolutely does what I wanted, and I could be persuaded that it's an easier approach.

      I have a pattern. I typically do things the hard way first, and this often includes PowerShell, too. And as this is PowerShell, there's also often, if not always, more than one way to accomplish the same goal. Thank you for the reminder regarding the Minute and Second parameters this morning. I enjoyed the refresher! Here's another example, and simpler than what was used in this article, as well. It's the same idea as yours, with only a small change.

      (Get-Date -Minute 59 -Second 59).AddSeconds(1)

  1. Luc FULLENWARTH

    Thanks to Tommy for sharing your experience!
    Thanks to Shane for sharing another way to do it!
    --------------------
    Blogging is not just about sharing.
    It is also taking the risk of exposing ourselves and accepting to be vulnerable to other points of view.
    A big thumb to those who dare and are blogging!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.