Adding lots of useful DateTime functions

Here are a couple of really useful methods you could add to the [DateTime] type.
 
Even if some are not exactly what you need it provides a framework for adding your own.
 
I’ve added them by putting the code below into a file in $PSHome named
DateTime.Types.ps1xml
Then in my $PSHome\Profile.ps1 I have the line
Update-TypeData (Join-Path $PSHome ‘DateTime.Types.ps1xml’)
Now anyone on the machine with a PowerShell Session has access to the extensions.
<?xml version="1.0" encoding="utf-8" ?>
<!--<# *******************************************************************

  This extension adds complex date manipulation functionality to
  [system.datetime] data types.  Specifically 2 Methods are added.  Each
  returns a [system.datetime] object with the requested value just like
  native methods such as AddYears or AddDays.

  AddBusinessDays([int]$Count)

    Returns the business day $Count days from $this date where 1 means
    next and -1 is prior.  Zero is treated opposite of TruncateTo('BD')
    i.e. it returns $this or the following Monday.  When the date falls
    on a weekend and $Count is not zero Monday or Friday are counted.
    That is <a-Sunday-date>.AddBusinessDays(1) results in Monday and
    <a-Sunday-date>.AddBusinessDays(-1) results in Friday.

  TruncateTo([string]$Part[,[int]$Offset])

    Truncates $this date to the date part named.

      Examples using:      $mydate = Get-Date '12/31/1999 17:47:12.345'
        $mydate.TruncateTo('Year')     Returns 01/01/1999 00:00:00.000
        $mydate.TruncateTo('QY')       Returns 10/01/1999 00:00:00.000
        $mydate.TruncateTo('BD')       Returns 12/31/1999 00:00:00.000
        $mydate.TruncateTo('Second')   Returns 12/31/1999 17:47:12.000
        $mydate.TruncateTo('Y',0)      Returns 01/01/1999 00:00:00.000
        $mydate.TruncateTo('FBDY',1)   Returns 01/03/2000 00:00:00.000
        $mydate.TruncateTo('18:00',1)  Returns 12/31/1999 18:00:00.000

    $Part is the part of the date to truncate to.  Either the abbreviated
          or long (FullySpelledOut) version of the part can be used.
               Y - Year
               M - Month
               D - Day
               H - Hour
              MI - Minute
               S - Second
          ******** Parts of a year
              HY - HalfYear       (Jan,Jul)
              QY - QuarterYear    (Jan,Apr,Jul,Oct)
            FBDY - FirstBusinessDayYear
          ******** Parts of a month
              HM - HalfMonth      (1st or 16th)
            FBDM - FirstBusinessDayMonth
          ******** Parts of a day
              HD - HalfDay        Midnight or Noon (12 hr period)
              TD - ThirdDay       Midnight, 8:am, 4:pm
              QD - QuarterDay     Midnight, 6:am, Noon, 6:pm
              BD - BusinessDay    BOD Today or most recent Fri on Sat or Sun
           hh:mm - SPECIFIC TIME  Some examples: "07:30", "14:00", "23:59"
          ******** Parts of an hour
              HH - HalfHour       :00 or :30 (30 minute period)
              TH - ThirdHour      :00, :20, :40 (20 minute periods)
              QH - QuarterHour    :00, :15, :30, :45 (15 minute periods)
              SH - SixthHour      :00, :10, :20, ... (10 minute periods)
          ******** Parts of a week
             Sun - Sunday         Truncates to most recent Sunday
             Mon - Monday
             Tue - Tuesday
             Wed - Wednesday
             Thu - Thursday
             Fri - Friday
             Sat - Saturday

    Note: Truncation without an offset will never return a future date.

    $Offset ..defaults to 0.  When non-zero, adds that number of $part
              periods to the result.  For example ('Y',2) first truncates
              to the current year then adds 2 years.  ('QH',1) used with
              17:37 yields 17:45 and ('QH',3) gives 18:15.
********************************************************************#> -->
<Types>
  <Type>
    <Name>System.DateTime</Name>
    <Members>
      <ScriptMethod>
        <Name>AddBusinessDays</Name>
        <Script>
    param ([int]$Count,[datetime]$From = $([datetime]::MinValue))
    # Use this if params don't seem to get passed 
    #[int]$Count = $args[0] 
    #[datetime]$From = [datetime]::MinValue 
    if ($From -eq [datetime]::MinValue) {$From = $This.AddDays(0)}
    while (!$Count -and 1..5 -notcontains $From.DayOfWeek.value__) {
        $From = $From.AddDays(1)}
    if ($Count -lt 0) {
      while ($Count++) {
        $From = $From.AddDays(-1)
        while (1..5 -notcontains $From.DayOfWeek.value__) {
          $From = $From.AddDays(-1)}
      }}
    else {
      while ($Count--) {
        $From = $From.AddDays(1)
        while (1..5 -notcontains $From.DayOfWeek.value__) {
          $From = $From.AddDays(1)}
      }}
    $From
        </Script>
      </ScriptMethod>
      <ScriptMethod>
        <Name>TruncateTo</Name>
        <Script>
    param ([string]$Part,[int]$Offset,[switch]$KeepTime)
    # Use this if params don't seem to get passed 
    #[string]$Part = $args[0] 
    #[int]$Offset = $args[1] 
    #[switch]$KeepTime = $args[2] 
    $Date = $this.Date
    $Time = $this.TimeOfDay
    $Year = $Date.AddDays((1-$this.DayOfYear))
    $DowB = [datetime]'1995/01/01'
    $Days = [math]::Truncate(($Date - $DowB).TotalDays)
    $Sign = [math]::Sign([math]::Sign($Days+1)-1)
    switch -regex ($Part) {
      # Parts of DateTime 
      '^(0?1|10|Y|Year)$' {
          $Year.AddYears($Offset)}
      '^(0?2|20|M|Month)$' {
          ($Date.AddDays((1-$Date.Day))).AddMonths($Offset)}
      '^(0?3|30|D|Day)$' {
          $Date.AddDays($Offset)}
      '^(0?4|40|H|Hour)$' {
          $Date.AddHours($Time.Hours+$Offset)}
      '^(0?5|Mi|Minute)$' {
          $Date.AddMinutes([math]::Truncate($Time.TotalMinutes)+$Offset)}
      '^(0?6|S|Second)$' {
          $Date.AddSeconds([math]::Truncate($Time.TotalSeconds)+$Offset)}
      # Parts of Year 
      '^(11|HY|HalfYear)$' {
          $Year.AddMonths(([math]::Truncate(($Date.Month -1)/6)+$Offset)*6)}
      '^(12|QY|QuarterYear)$' {
          $Year.AddMonths(([math]::Truncate(($Date.Month -1)/3)+$Offset)*3)}
      '^(19|FBDY|FirstBusinessDayYear)$' {
          $t1 = $t2 = $Year
          while (1..5 -notcontains $t1.DayOfWeek.value__)
            {$t1 = $t1.AddDays(1)}
          if ($Date.DayOfYear -lt $t1.DayOfYear) {
            $t2 = $t2.AddYears($Offset - 1)}
          else {
            $t2 = $t2.AddYears($Offset)}
          while (1..5 -notcontains $t2.DayOfWeek.value__)
            {$t2 = $t2.AddDays(1)}
          $t2}
      # Parts of Month 
      '^(21|HM|HalfMonth)$' {
          $t = $Date.AddMonths([math]::Truncate($Offset/2))
          if ($t.Day -ge 16) {
            switch ($Offset%2) {
              1 {$t.AddDays(1-$t.Day).AddMonths(1)}
             -1 {$t.AddDays(1-$t.Day)}
              0 {$t.AddDays(16-$t.Day)}}}
          else {
            switch ($Offset%2) {
              1 {$t.AddDays(16-$t.Day)}
             -1 {$t.AddDays(16-$t.Day).AddMonths(-1)}
              0 {$t.AddDays(1-$t.Day)}}}}
      '^(29|FBDM|FirstBusinessDayMonth)$' {
          $t1 = $t2 = $Date.AddDays(1-$Date.Day)
          while (1..5 -notcontains $t1.DayOfWeek.value__)
            {$t1 = $t1.AddDays(1)}
          if ($Date.Day -ge $t1.Day) {
            $t2 = $t2.AddMonths($Offset)}
          else {
            $t2 = $t2.AddMonths($Offset - 1)}
          while (1..5 -notcontains $t2.DayOfWeek.value__) {
            $t2 = $t2.AddDays(1)}
          $t2}
      # Parts of Day 
      '^(31|HD|HalfDay)$' {
          $Date.AddHours(([math]::Truncate($Time.Hours/12)+$Offset)*12)}
      '^(32|TD|ThirdDay)$' {
          $Date.AddHours(([math]::Truncate($Time.Hours/8)+$Offset)*8)}
      '^(33|QD|QuarterDay)$' {
          $Date.AddHours(([math]::Truncate($Time.Hours/6)+$Offset)*6)}
      '^(39|BD|BusinessDay)$' {
          $t = $Date
          while (1..5 -notcontains $t.DayOfWeek.value__)
            {$t = $t.AddDays(-1)}
          $this.AddBusinessDays($Offset,$t)}
      # Time of day 
      '^[0-9][0-9]?:[0-9][0-9]$' {
          $TimeOfDay = [TimeSpan]$_
          $(if ($this.TimeOfDay -gt $TimeOfDay) {
              $this.date + $TimeOfDay}
            else {$this.date.AddDays(-1) + $TimeOfDay}).AddDays($Offset)}
      # Parts of Hour 
      '^(41|HH|HalfHour)$' {
          $Date.AddMinutes(([math]::Truncate($Time.TotalMinutes/30)+$Offset)*30)}
      '^(42|TH|ThirdHour)$' {
          $Date.AddMinutes(([math]::Truncate($Time.TotalMinutes/20)+$Offset)*20)}
      '^(43|QH|QuarterHour)$' {
          $Date.AddMinutes(([math]::Truncate($Time.TotalMinutes/15)+$Offset)*15)}
      '^(44|SH|SixthHour)$' {
          $Date.AddMinutes(([math]::Truncate($Time.TotalMinutes/10)+$Offset)*10)}
      # Parts of Week 
      '^(71|Sun|Sunday)$' {
          $DowB.AddDays(([math]::Truncate(($Days -(0+$Sign))/7)+$Offset+$Sign)*7+0)}
      '^(72|Mon|Monday)$' {
          $DowB.AddDays(([math]::Truncate(($Days -(1+$Sign))/7)+$Offset+$Sign)*7+1)}
      '^(73|Tue|Tuesday)$' {
          $DowB.AddDays(([math]::Truncate(($Days -(2+$Sign))/7)+$Offset+$Sign)*7+2)}
      '^(74|Wed|Wednesday)$' {
          $DowB.AddDays(([math]::Truncate(($Days -(3+$Sign))/7)+$Offset+$Sign)*7+3)}
      '^(75|Thu|Thursday)$' {
          $DowB.AddDays(([math]::Truncate(($Days -(4+$Sign))/7)+$Offset+$Sign)*7+4)}
      '^(76|Fri|Friday)$' {
          $DowB.AddDays(([math]::Truncate(($Days -(5+$Sign))/7)+$Offset+$Sign)*7+5)}
      '^(77|Sat|Saturday)$' {
          $DowB.AddDays(([math]::Truncate(($Days -(6+$Sign))/7)+$Offset+$Sign)*7+6)}
      default {
        # The numeric syntax is for testing and unsupported numbers return $null
        if ($_ -notmatch '^[0-9][0-9]?$') {
          throw ("Expecting one of`nY M D H Mi S HY QY FBDY HM FBDM " +
                 "HD TD QD hh:mm HH TH QH SH BD SUN MON TUE WED THU FRI SAT`n" +
                 "Year Month Day Hour Minute Second HalfYear " +
                 "QuarterYear FirstBusinessDayYear HalfMonth`n" +
                 "FirstBusinessDayMonth HalfDay ThirdDay QuarterDay " +
                 "BusinessDay HalfHour ThirdHour QuarterHour SixthHour`n" +
                 "Sunday Monday Tuesday Wednesday Thursday Friday Saturday")}}
    }
         </Script>
      </ScriptMethod>
    </Members>
  </Type>
</Types>

P.S. Looking at the code reveals that $part can be a number (not documented).
    This feature has 2 purposes.
    1) I have a scalar valued function in SQL Server with similar functionality.
       Because that is used in queries I wanted a more efficient arg in its case statement.
       These numbers match those...
    2) It makes for easier testing because I don't need to code all the valid type names.
       For example:

    
$e = [datetime]'2009/11/16 22:47:31.562'
1..80|%{'{0:D2}' -f $_}|
  %{if($e.TruncateTo($_))
    {   "$_ =" +
        " $($e.TruncateTo($_,-2).ToString('yyyy/MM/dd HH:mm:ss.f ddd')) |" +
        " $($e.TruncateTo($_,-1).ToString('yyyy/MM/dd HH:mm:ss.f ddd')) |" +
        " $($e.TruncateTo($_, 0).ToString('yyyy/MM/dd HH:mm:ss.f ddd')) |" +
        " $($e.TruncateTo($_, 1).ToString('yyyy/MM/dd HH:mm:ss.f ddd')) |" +
        " $($e.TruncateTo($_, 2).ToString('yyyy/MM/dd HH:mm:ss.f ddd'))"}}

One Trackback

  1. By T-SQL date formulas « Rick Bielawski on December 8, 2011 at 7:19 am

    […] is a PowerShell version that does more here: https://rickbielawski.wordpress.com/2009/03/13/adding-lots-of-useful-datetime-functions/ This shows how easy it is to do in […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: