2

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)

Andrew
  • 221
  • 1
  • 10
  • 1
    Take off the `[parameter]` before `[string]$Date` since youve already defined the parameter by casting a datatype of `[string]` to `$Date`. So `Calculate-Date -AddYears 0 -AddMonths 0 -AddDays 2` works because, `$Date` parameter wasnt specified to be evaluated for. – Abraham Zinala Apr 14 '21 at 02:11
  • Thanks. This works, although I don't fully understand the logic. If I specify the variable type, in this case [string], there's no need to add [parameter()] if it's optional, but I must add [parameter(Mandatory = $true)], even if I specify [int]? – Andrew Apr 14 '21 at 02:41
  • 2
    See @Mklement0's detailed explanation but, to answer your question, no. Unless you want to provide specific attributes such as the parameter to be mandatory, yes. Otherwise you can just use `[int]AddYears` by itself. `[Parameter()]` is completely optional here if nothing else needs to be done to any of your variables/parameters. – Abraham Zinala Apr 14 '21 at 02:46

2 Answers2

3

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 }.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks for your explantion and links. It seems that it might be best to avoid [parameter()] unless required to specify it is mandatory to avoid a potential typo. I've marked you answer as accepted, and would also accept the reply of Abraham Zinala. I will have a look at my former posts too. – Andrew Apr 14 '21 at 04:23
  • Unfortunately I now have the problem that ```Calculate-Date -AddYears 0 -AddMonths 0 -AddDays 2``` doesn't work after I removed [parameter]. The error is in a foreign language, so I can't translate it literally, but it mentions the Date parameter cannot be bound and something about System.DateTime. I've also been trying to add a Format parameter to offer more choice than yyyy-MM-dd, but have issues that I'm guessing are related to the Date parameter problem. – Andrew Apr 14 '21 at 05:21
  • Just to mention I added [parameter()] also, but the error remains the same. – Andrew Apr 14 '21 at 05:26
  • 1
    @Andrew, please try with the streamlined version of the function I've just added to the answer. It not only simplifies the code, but applies _culture-invariant_ parsing of your date string, which differs from what your code currently does (not sure about the specific call, though). If that doesn't help, please post the error message here, even if it is in a foreign language. Alternatively, run your command as follows: `& { [cultureinfo]::CurrentUICulture = 'en-US'; Calculate-Date -AddYears 0 -AddMonths 0 -AddDays 2 } `, which should give you an English error message. – mklement0 Apr 14 '21 at 12:58
  • 1
    mklement0, the streamlined version works great. I notice that you changed $Date to datetime and added a default value. Less code is always better and I'll keep this feature in mind when writing future functions. Joma below has also been of great assistance in providing a version that works with various "cultures" so I will be reading up on their relationship in the PowerShell scheme of things. – Andrew Apr 16 '21 at 00:33
  • Glad to hear it, @Andrew. The short of it is: if you stay in the realm of PowerShell type constraints / casts, such as `[datetime]` and PowerShell's string interpolation (e.g., `"$(Get-Date)"`, you needn't worry about culture differences, because everything is based on the _invariant_ culture (which itself is based on the US-English culture, but is guaranteed to never change). – mklement0 Apr 16 '21 at 01:39
  • 1
    I'm actually interested in being in able to specifiy the culture. I think I worked out how to do it and your method of using Get-Date as a default value helped out a lot. I'm in the process of editing my original comment to show my "extended" version. – Andrew Apr 16 '21 at 02:12
1

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

Joma
  • 3,520
  • 1
  • 29
  • 32
  • `Return` isn't needed, just *syntactical sugar*. – Abraham Zinala Apr 14 '21 at 03:15
  • Joma, your code looks interesting, but could you please explain what's happening? – Andrew Apr 14 '21 at 04:25
  • Joma, I've added a format parameter so I can do things like get "yy" or "dddd". Would it be possible to add a optional parameter to set the culture (Looking at your code, it *seems* possible!) For example, en-au or ja-jp, if missing, use the default culture. – Andrew Apr 14 '21 at 05:58
  • Joma, just saw your edits now. Thanks for the comments and links. Would any of it be affected by the framework version? – Andrew Apr 14 '21 at 06:49
  • 1
    Updated code and comments. I hope it is useful. I have installed Powershell 7.1 it uses .NET Core 3.1. – Joma Apr 14 '21 at 07:17
  • Joma, thanks again for your help. Unfortunately, the new code shows an error for the Japanese dates. I noticed that Get-Culture returns ja-JP and changed $jpJPCulture to ja-JP, but still got an error. Perhaps $jpJPDate is different too. I'm not about how it's referred to programmatically but "culturally" dates are year, month and day (and there might be other stuff involved too. A Japanese specialist in .NET would probably know, but I'm mainly a "VB-family" language guy). Anyway, I hope it's not too late in your timezone. I'm about to head home and will check my inbox tomorrow. – Andrew Apr 14 '21 at 08:23
  • 1
    You are correct. The name of the culture is ja-JP. Fixed code. $jaJPDate = "2021/04/14" (correct value) and $jaJPCulture = "ja-JP" (correct culture name). Sorry for my mistake. – Joma Apr 14 '21 at 08:53
  • Joma, I'm sorry not to reply earlier, but I actually had a holiday that I'd forgotten about. Your new version works great. I'm wondering if it's possible just to have a "DestinationCulture" version, which I could then extract what I need with a format parameter. For example, could I specify "es-ES" and "dddd" with a $Fomat parameter to output "viernes". (I think this is Spanish for Friday!). Out of curioisity, would you use "es-ES" or something else? – Andrew Apr 16 '21 at 01:14
  • I think I worked it out. I'll edit my original comment to add my new version. – Andrew Apr 16 '21 at 02:04
  • 1
    Nice. You already did it. I hope I have been helpful. – Joma Apr 16 '21 at 06:47
  • You were very helpful Joma. I appreciate it very much. – Andrew Apr 22 '21 at 06:10