Matthew Watt
Matthew Watt

Reputation: 11

PowerShell: Multiple ambiguous overloads found for "op_Subtraction" and the argument count: "2"

Trying to determine if AD accounts have been modified in the last 2 hours.

If I manually do a Get-ADUser and then compare $ObjDelta = ((Get-Date) - ($i.Modified)) I can successfully check the "Hours" value.

Days : 0 Hours : 2 Minutes : 45 Seconds : 10 Milliseconds : 321 Ticks : 99103217697 TotalDays : 0.114702798260417 TotalHours : 2.75286715825 TotalMinutes : 165.172029495 TotalSeconds : 9910.3217697 TotalMilliseconds : 9910321.7697

Yet when I put it in a script with

$Output = Foreach ($i in $Imported) {
    $ObjDelta = ((Get-Date) - ($i.Modified))
    If ($ObjDelta.Hours -gt "1") {
        <Do things here>
    }

I get a running error on each $i of

Multiple ambiguous overloads found for "op_Subtraction" and the argument count: "2". At line:1 char:1

  • $ObjDelta = ((Get-Date) - ($i.Modified))

CategoryInfo : NotSpecified: (:) [], MethodException FullyQualifiedErrorId : MethodCountCouldNotFindBest

I have confirmed that the "Modified" value is populated on these accounts.

Any thoughts?

Upvotes: 1

Views: 835

Answers (2)

mklement0
mklement0

Reputation: 437197

tl;dr

As you've discovered yourself, you need to convert $i.Modified to a [datetime] instance explicitly, using the rules of the current culture, given that the data came from a CSV presumably created with Export-Csv:

$ObjDelta = (Get-Date) - (Get-Date $i.Modified) # or: [datetime]::Parse($i.Modified)

Note:

  • The above assumes that the CSV data was created while the same culture as your current one was in effect. If not, see the next section for a solution.

  • Only if your current culture happens to be en-US (US-English) would
    [datetime] $i.Modified, i.e. a simple cast be sufficient, for the reasons explained in the next section.


Background information:

As js2010's helpful answer explains, a subtraction operation (-) with a [datetime] instances as the LHS requires as its RHS either another [datetime] instance or a [timespan] instance. In the former case the result is a time span (i.e. a [timespan] instance), in the latter a new point in time (i.e. a different [datetime] instance).

The reason that (Get-Date) - $i.Modified didn't work in your case is that you loaded your data from a CSV file, presumably with Import-Csv, and CSV data is inherently untyped, or, more accurately, invariably [string]-typed.

While PowerShell generally attempts automatic conversions to suitable operand types, it cannot do so in the case at hand, because it is possible to convert a string to either of the two supported RHS types, which results in the error message complaining about ambiguous overloads you saw.[1]

PowerShell supports convenient from-string conversions simply using a cast to the target type (placing a type literal such as [datetime] before the value to convert), which translate into calls to the static ::Parse() method behind the scenes (if exposed by the target type).

For instance, [datetime] '1970/01/01' is translated to
[datetime]::Parse('1970/01/01', [cultureinfo]::InvariantCulture)

Note the use of [cultureinfo]::InvariantCulture: PowerShell by design, if available, requests use of this - as the name suggests - invariant culture, which is based on, but distinct from, the US-English culture.

By contrast:

  • Passing only a string to [dateteime]::Parse() uses the current culture's formats; e.g., [datetime]::Parse('6.12') is interpreted as 12 June (month-first with culture en-US (US-English) in effect, and as 6 December (day-first) in a culture such as fr-FR (French).

    • The solution above therefore only works if the CSV data was created with the same (or at least a compatible) culture in effect as the one in effect when the data is read. If this assumption doesn't hold, you'll have to parse the date/time strings with an explicit format string that matches the data, using [datetime]::ParseExact(); e.g.:

      # Parse as day.month.year
      [datetime]::ParseExact('6.12.2023', 'd\.M\.yyyy', $null)
      
  • Curiously - and regrettably - when passing strings as arguments to binary cmdlets (as opposed to cmdlet-like scripts and functions written in PowerShell), it is also the current culture that is used, so that Get-Date 6.12 exhibits the same culture-dependent behavior as [datetime]::Parse('6.12').

    • This is a known inconsistency that will not be fixed, however, so as to preserve backward compatibility - see GitHub issue #6989.

    • In the code at the top, this behavior is taken advantage of; however, you may prefer use of [datetime]::Parse($i.Modified) to avoid ambiguity.

The reason that culture-sensitive parsing of $i.Modified is necessary in your case (Get-Date $i.Modified or [datetime]::Parse($i.Modified)) is the - unfortunate - behavior of Export-Csv (and its in-memory counterpart, ConvertTo-Csv) to invariably use the current culture when stringifying dates ([datetime]) and (fractional) numbers (typically, [double]):

  • This hampers the portability of CSV data generated this way.

    • Note what while there is a -UseCulture switch, the only culture-sensitive aspect it controls is the separator character: by default, it is always , (i.e., culture-insensitive, ironically); with -UseCulture it is the current culture's list separator, such as ; in the French culture.

    • GitHub issue #20383, in the context of discussing improvements to the stringification of complex objects, summarizes the culture-related problems of the CSV cmdlets; ideally, by default they would be culture-invariance consistently and comprehensively, with (consistent and comprehensive) culture-sensitivity only coming into play on demand, with -UseCulture; sadly, this is again not an option if backward compatibility must be maintained.

  • The only way to avoid culture-sensitivity is to generate string representations of the property (column) values explicitly, in the simplest case via using a [string] cast, which stringifies with the invariant culture; e.g.:

    [pscustomobject] @{ PropA = 'string'; PropB = Get-Date; PropC = 1.5 } |
      ForEach-Object {
        $oht = [ordered] @{} # helper hashtable for constructing an all-strings clone of the object
        foreach ($p in $_.psobject.Properties) {
          # Use PowerShell's [string] cast, which uses *culture-invariant* stringification
          $oht[$p.Name] = [string] $p.Value
        }
        # Construct and output the all-strings clone.
        [pscustomobject] $oht
      } |
      ConvertTo-Csv      
    
    • The above yields culture-invariant CSV data; e.g.:

      "PropA","PropB","PropC"
      "string","12/06/2023 15:04:02","1.5"
      
    • When such culture-invariant data is parsed, regular PowerShell casts can then be used for from-string conversion (e.g. [datetime] $i.Modified or [double] $i.Ratio)


[1] Note that during overload resolution (for the call to the op_Subtraction() method call underlying the operator in this case), PowerShell generally only consults the type of the arguments, not also their content.

Upvotes: 1

js2010
js2010

Reputation: 27428

The second argument can be [timespan] or [datetime]:

[datetime]::op_Subtraction

OverloadDefinitions
-------------------
static datetime op_Subtraction(datetime d, timespan t)
static timespan op_Subtraction(datetime d1, datetime d2)
(get-date) - [datetime]'12/3'


Days              : 1
Hours             : 0
Minutes           : 1
Seconds           : 24
Milliseconds      : 542
Ticks             : 864845423929
TotalDays         : 1.00097849991782
TotalHours        : 24.0234839980278
TotalMinutes      : 1441.40903988167
TotalSeconds      : 86484.5423929
TotalMilliseconds : 86484542.3929

Upvotes: 2

Related Questions