Optional parameter to calculate dates

Viewed 151

I want to create a generic function to calculate dates. Here is my attempt.

The function should add or subtract years/months/days from either a specified date or today. I've got the "calculate from today" part working, but do not know how to calculate from a specified date.

Function Calculate-Date {
    Param(
    [parameter][string]$Date,
    [parameter(Mandatory = $True)][int]$AddYears,
    [parameter(Mandatory = $True)][int]$AddMonths,
    [parameter(Mandatory = $True)][int]$AddDays)

    If ($Date -eq $null) {

        (Get-Date).AddYears($AddYears).AddMonths($AddMonths).AddDays($AddDays).ToString("yyyy-MM-dd")

    }

    Else {

        $CalculateDate = Get-Date $Date

        ($CalculateDate).AddYears($AddYears).AddMonths($AddMonths).AddDays($AddDays).ToString("yyyy-MM-dd")

    }

}

This works.

Calculate-Date -AddYears 0 -AddMonths 0 -AddDays 2

This throws an error.

Calculate-Date -Date "2021-01-01" -AddYears 0 -AddMonths 0 -AddDays 2

Edit: Thanks to everybody, I was not just only able to overcome the error, but also simplify my function and add some extra parameters that I thought might be useful.

Function Calculate-Date {

  Param(
    [datetime]$Date = (Get-Date), # default to now
    [int]$AddYears,
    [int]$AddMonths,
    [int]$AddDays,
    [string]$Format = 'yyyy-MM-dd', # default format, change as required 
    [string]$Culture = (Get-Culture) # default culture
    )

   $Date.AddYears($AddYears).AddMonths($AddMonths).AddDays($AddDays).ToString($Format, [CultureInfo]::CreateSpecificCulture($Culture))

}

The differences to my original code are

  1. I removed [parameter] and [parameter(Mandatory = $True). To be honest, I thought [parameter(Mandatory = $True) was necessary for the year, month and day because I was unfamiliar with default values.

  2. I thought it might be useful to add a format parameter so I could do things like extract the weekday (for example, "dddd") or add specific separators (for example "yyyy年m月d日").

  3. Having done that, I thought it would be good to specifiy the output' language or "culture". So "Friday" could become "viernes" if I used "es-ES".

Assuming my extended version works on your PC, here are some examples.

Calculate-Date

Should return the current date formatted as "yyyy-MM-dd"

Calculate-Date -AddYears 1 -AddMonths -2 -AddDays 3

Add a year, subtract 2 months, and add 3 days, with the same default format

Calculate-Date -AddYears 1 -AddMonths -2 -AddDays 3 -Format 'dddd'

Returns the weekday

Calculate-Date -AddYears 1 -AddMonths -2 -AddDays 3 -Format 'dddd' -Culture 'es-ES'

Does the same in Spanish (if not already the default)

2 Answers

Unlike in C#, use of attributes in PowerShell always requires (...) - even if no properties of the attribute are being set.

Therefore, change [parameter] to [parameter()].

That said, not setting properties of a Parameter attribute means that its use is optional - making the parameter itself an optional one - so you can simply omit it - see the relevant section of the conceptual About Functions Advanced Parameters help topic.

As an aside: Since PowerShell version 3, there is syntactic sugar that simplifies setting Boolean properties of attributes to $true: [Parameter(Mandatory = $true)] can be shortened to [Parmater(Mandatory)], for instance.


As for what you tried:

In PowerShell, [parameter] is a type literal, which on the LHS of a (parameter) variable declaration or assignment acts as a type constraint - similar to a cast, except that it is applied every time a value is assigned to the variable (which with parameter variables typically only happens once, on invocation).

That is, an attempt is made to convert any value being assigned to that variable to the specified type.

[parameter] [string]$Date effectively applies two type constraints: first, the argument being passed via -Date is converted to a string ([string]) - which always succeeds - and then to [parameter] - which fails, just like the cast [parameter] "2021-01-01" would.

For more information about PowerShell's type-literal notation ([<type>]) and its uses, see this answer.


A streamlined version of your function:

Function Get-CalculatedDate {
  Param(
    [parameter()]          [datetime]$Date = (Get-Date), # default to now
    [parameter(Mandatory)] [int]$AddYears,
    [parameter(Mandatory)] [int]$AddMonths,
    [parameter(Mandatory)] [int]$AddDays
  )

   $Date.AddYears($AddYears).AddMonths($AddMonths).AddDays($AddDays).ToString("yyyy-MM-dd")

}
  • You can directly type your -Date parameter $Date parameter variable as [datetime] so that passing an argument such as "2021-01-01" is directly converted to the equivalent [datetime] instances.

    • Note that such a conversion uses the invariant culture, and therefore works independently of which culture is currently in effect.

    • That is, passing "2021-07-01" to -Date is effectively the same as cast
      [datetime] "2021-07-01", which correctly results in a [datetime] instance representing 1 July 2021.

    • Caveats:

      • The invariant culture is based on the US-English culture, so that month-first interpretation of date strings such as '7/1/2021' is performed: [datetime] '7/1/2021' always yields 1 July 2021, irrespective of the current culture.

      • Due to a historical bug that won't be fixed so as to preserve backward compatibility (see GitHub issue #3348, compiled cmdlets (as opposed to functions written in PowerShell), do apply the rules of the current culture for parameters with culture-sensitive data types when converting from strings; for instance, the interpretation of '7/1/2021' in Get-Date -Date '7/1/2021'depends on the current culture, and with en-GB (UK English) in effect, day-first interpretation is performed resulting in 7 January 2021.[1]

  • You can also define the parameter (variable) with a default value, which is simply a call to Get-Date in this case, so as to default to the current point in time.


[1] Compare the output from [datetime] '7/1' to the following : & { $prev = [cultureinfo]::CurrentCulture; [cultureinfo]::CurrentCulture = 'en-GB'; Get-Date -Date '7/1'; [cultureinfo]::CurrentCulture = $prev }.

I'm using .NET classes

Example Code - Updated with CultureNames parameters

Function Get-CalculatedDate {
    Param(
        [parameter()][string]$Date, #optional parameter
        [parameter()][int]$AddYears = 0, #optional parameter / default value 0 if not present
        [parameter()][int]$AddMonths = 0, #optional parameter / default value 0 if not present
        [parameter()][int]$AddDays = 0, #optional parameter / default value 0 if not present,
        [parameter()][string]$SourceCulture, #optional parameter / $Date will be parsed with the culture format.
        [parameter()][string]$DestinationCulture #optional parameter / CalculatedDate will be formatted with the culture format.
    )
    
    If ([System.String]::IsNullOrWhiteSpace($Date)) # Checking if $Date(System.String) value is null/empty/whitespace
    {
        $dt = [System.DateTime]::Now #Assign actual date from local computer as default date.
    }
    else 
    {
        [System.Globalization.CultureInfo] $SourceCultureInfo = [System.Globalization.CultureInfo]::new($SourceCulture) # Get an instance of IFormatProvider, creating new CultureInfo with the $SourceCulture parameter.
        $dt = [System.DateTime]::ParseExact($date, $SourceCultureInfo.DateTimeFormat.ShortDatePattern , $provider); # Parsing date with the format of $SourceCultureInfo - convert System.String to System.DateTime.
    }

    [System.Globalization.CultureInfo] $DestinationCultureInfo = [System.Globalization.CultureInfo]::new($DestinationCulture) # Get an instance of IFormatProvider, creating new CultureInfo with the $DestinationCulture parameter.
    return $dt.AddYears($AddYears).AddMonths($AddMonths).AddDays($AddDays).ToString($DestinationCultureInfo.DateTimeFormat.ShortDatePattern, $DestinationCultureInfo) #Add days, months, year to System.DateTime -> convert to System.String using the format of $DestinationCultureInfo-> and returning the string.
}


# Calling the function
$invariantDate = "04/14/2021" # Date in invariant format.
$enUsDate = "4/14/2021" # Date in en-US culture format.
$jaJPDate = "2021/04/14" # Date in jp-JP culture format.

$invariantCulture = "" # Invariant culture name.
$enUSCulture = "en-US" # English, United States culture name.
$jaJPCulture = "ja-JP" # Japan, Japanesse culture name.


Write-Host "█ No parameters"
Get-CalculatedDate # from local computer's invariant date to local computer's invariant date

Write-Host "█ From Invariant date"

Get-CalculatedDate $invariantDate -AddYears 4 -DestinationCulture $invariantCulture
Get-CalculatedDate $invariantDate -AddYears 4 -DestinationCulture $enUSCulture
Get-CalculatedDate $invariantDate -AddYears 4 -DestinationCulture $jaJPCulture

Write-Host "█ From en-US date"
Get-CalculatedDate $enUsDate -AddYears 4 -AddMonths 8 -SourceCulture $enUSCulture -DestinationCulture $invariantCulture
Get-CalculatedDate $enUsDate -AddYears 4 -AddMonths 8 -SourceCulture $enUSCulture -DestinationCulture $enUSCulture
Get-CalculatedDate $enUsDate -AddYears 4 -AddMonths 8 -SourceCulture $enUSCulture -DestinationCulture $jaJPCulture

Write-Host "█ From jp-JP date"
Get-CalculatedDate $jaJPDate -AddYears 4 -AddMonths 8 -AddDays 11 -SourceCulture $jaJPCulture -DestinationCulture $invariantCulture
Get-CalculatedDate $jaJPDate -AddYears 4 -AddMonths 8 -AddDays 11 -SourceCulture $jaJPCulture -DestinationCulture $enUSCulture
Get-CalculatedDate $jaJPDate -AddYears 4 -AddMonths 8 -AddDays 11 -SourceCulture $jaJPCulture -DestinationCulture $jaJPCulture

References

DateTime.Now
DateTime.ParseExact
CultureInfo.InvariantCulture
DateTime.ToString
DateTime.ToString
Custom date and time format strings

Related