2

Let's say I have an object called $data which contains the following:

  name    stock  
 ------- ------- 
  item1      10  
  item2      10  
  item3      10  

How do I subtract by 1 item2 stock value?

I tried with this:

$data.stock = $data.stock - 1 | Where-Object name -contains "item2"

But I get the following error:

Method invocation failed because [System.Object[]] does not contain a method named 'op_Subtraction'.

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
shellwhale
  • 820
  • 1
  • 10
  • 34

3 Answers3

3

EDIT

I can only guess the structure of your data, but it looks like it's an array.

Now I know how to instintiate the variable $data. So this is my attempt:

$data = ConvertFrom-Csv -Header name, stock @'
item1,10
item2,10
item3,10
'@
$data | Format-Table -AutoSize
$data | Where-Object name -eq 'item2' | ForEach-Object { $_.stock -= 1 }
$data | Format-Table -AutoSize

The output:

name  stock
----  -----
item1 10   
item2 10   
item3 10   

name  stock
----  -----
item1 10   
item2 9    
item3 10
Andrei Odegov
  • 2,925
  • 2
  • 15
  • 21
3

Andrei Odegov's helpful answer offers a PowerShell-idiomatic solution that also demonstrates the simplified, argument-based Where-Object syntax called comparison statement, introduced in PSv3.

In PSv4+ there is another option, using the .Where() and .ForEach() collection "operator" methods, which perform better; however, note that it requires the input collection to be in memory in full[1] (which is the case here):

 $data.Where({ $_.name -eq "Item2" }).ForEach({ $_.stock -= 1 })

Or, if you know that there will only be one match, more simply:

 $data.Where({ $_.name -eq "Item2" })[0].stock -= 1 

Also note how -eq rather than -contains is used, because $_.name is a scalar (single value) rather than a collection, and -contains is designed for collections.


As for what you tried:

It looks like you were trying to apply something akin to a Python-style list comprehension, which PowerShell doesn't support.

Instead, you must start with the whole collection, filter it down, and then apply the desired operation on the resulting elements, as shown.

I tried with this :
$data.stock = $data.stock - 1 | Where-Object name -contains "item2"

In PSv3+, accessing a property on a collection returns an array ([System.Object[]]) of the property values collected from the collection's elements.

Therefore, with your sample data, $data.stock - 1 is the equivalent of:

(10, 10, 10) - 1  # !! BROKEN: arrays don't support the "-" operator

Because arrays don't support the - operator (whose implementation is based on a static op_Subtraction method), you got the error message you saw.


[1] The PowerShell pipeline vs. all-in-memory processing:

Use of the pipeline is comparatively slow, but memory-throttling, which enables processing of collections that wouldn't fit into memory as a whole; e.g.:

ConvertFrom-Csv in.csv | Where-Object ... | ForEach-Object ... | Export-Csv out.csv

In the example above, each row from in.csv is processed individually and sent through the pipeline right away, and Export-Csv creates the output row by row.

That way, there is no need to read the input into memory as a whole.

By contrast, if do collection processing with (array-aware) operators / .Where()/.ForEach() methods / a foreach loop, you get better performance, but the input must be collected in memory in full, up-front, which is not always an option.

See the bottom section of this answer of mine for how the approaches compare in terms of performance (execution speed).

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • I might need to take a look at thoses collection "operators", looks great. You say it requires the input collection to be fully charged in memory and it makes me wonder in wich kind of cases input collections are not. Many thanks for the overall high quality answer by the way. – shellwhale May 13 '18 at 17:47
  • @shellwhale: Thanks; please see the footnote I've added. On a side note, I've suggested that the `.Where()` / `.ForEach()` _methods_ be surfaced as bona fide `-where` / `-foreach` PS _operators_ [on GitHub](https://github.com/PowerShell/PowerShell-RFC/pull/126). – mklement0 May 13 '18 at 18:20
-1
$data = import-csv data.csv
$data

$data[1].stock -=1
$data

ForEach ($Row in $data){
  if ($Row.Name -eq 'item2') { $Row.stock -=1 }
}
$data

$data -match 'item2'|%{$_.stock -=1}

Sample output

name  stock
----  -----
item1 10
item2 10
item3 10

item1 10
item2 9
item3 10

item1 10
item2 8
item3 10
  • The `$data -match 'item2'|%{$_.stock -=1}` solution (which could be simplified to `($data -match 'item2')[0].stock -= 1`) is alluringly short, but brittle, because it relies on matching the for-display string representations of the whole input objects. – mklement0 May 13 '18 at 17:32