Add properties to array - powershell

Let's say I have an array of objects:
$data = #(
[pscustomobject]#{Id1='1';Id2=51213412;ID3='00:01:00'}
[pscustomobject]#{Id1='1';Id2=51213412;ID3='00:02:00'}
[pscustomobject]#{Id1='1';Id2=51213412;ID3='00:04:00'}
[pscustomobject]#{Id1='1';Id2=51213412;ID3='00:04:00'}
[pscustomobject]#{Id1='1';Id2=51213412;ID3='00:02:00'}
[pscustomobject]#{Id1='2';Id2=11112314;ID3='00:01:00'}
[pscustomobject]#{Id1='2';Id2=11112314;ID3='00:02:00'}
[pscustomobject]#{Id1='2';Id2=11112314;ID3='00:03:00'}
[pscustomobject]#{Id1='2';Id2=11112314;ID3='00:01:00'}
[pscustomobject]#{Id1='3';Id2=12512521;ID3='00:01:00'}
[pscustomobject]#{Id1='2';Id2=11112314;ID3='00:02:00'}
[pscustomobject]#{Id1='4';Id2=42112521;ID3='00:01:00'}
[pscustomobject]#{Id1='2';Id2=11112314;ID3='00:03:00'}
[pscustomobject]#{Id1='3';Id2=12512521;ID3='00:02:00'}
[pscustomobject]#{Id1='4';Id2=42112521;ID3='00:02:00'}
[pscustomobject]#{Id1='1';Id2=51213412;ID3='00:03:00'}
[pscustomobject]#{Id1='4';Id2=42112521;ID3='00:01:00'}
[pscustomobject]#{Id1='3';Id2=12512521;ID3='00:05:00'}
[pscustomobject]#{Id1='4';Id2=42112521;ID3='00:03:00'}
[pscustomobject]#{Id1='3';Id2=12512521;ID3='00:01:00'}
[pscustomobject]#{Id1='2';Id2=11112314;ID3='00:02:00'}
[pscustomobject]#{Id1='4';Id2=42112521;ID3='00:01:00'}
[pscustomobject]#{Id1='2';Id2=11112314;ID3='00:03:00'}
[pscustomobject]#{Id1='3';Id2=12512521;ID3='00:02:00'}
[pscustomobject]#{Id1='3';Id2=12512521;ID3='00:04:00'}
[pscustomobject]#{Id1='1';Id2=51213412;ID3='00:05:00'}
[pscustomobject]#{Id1='4';Id2=42112521;ID3='00:01:00'}
[pscustomobject]#{Id1='3';Id2=12512521;ID3='00:05:00'}
[pscustomobject]#{Id1='5';Id2=53252352;ID3='00:01:00'})
I would like to convert property ID3, that is time to display only minutes so I can ultimately make a sum of them. I found a code that can convert it to minutes:
$minutes = foreach ($mins in $data.ID3) {
$TimeSpan = [System.TimeSpan]::Parse($mins)
[System.Math]::Round($TimeSpan.TotalMinutes,0)}
But now I get the results in $minutes variable. What is the fastest way to replace ID3 values with converted value?

Use a calculated property expression with Select-Object to select a new "column" based on the value of an existing property:
$data |Select-Object ID1,ID2,ID3,#{Name='Minutes';Expression={[math]::Round([timespan]::Parse($_.ID3).TotalMinutes, 0)}}
This will create a new object for every input object, each with the 3 properties from the input object and a new property Minutes with the parsed value
If you want to "overwrite" the existing ID3 property, simply omit it from the list of property names and supply it as the name for the calculated property instead:
$data |Select-Object ID1,ID2,#{Name='ID3';Expression={[math]::Round([timespan]::Parse($_.ID3).TotalMinutes, 0)}}

If you want to update the object's values, you can enumerate them and reassign them like this:
foreach($i in $data) {
$i.ID3 = ([timespan] $i.ID3).Minutes
}
However, since you mention you want the sum of them, you might be just looking for this:
[timespan]::FromMilliseconds(
[System.Linq.Enumerable]::Sum(
[int[]] ([timespan[]] $data.ID3).TotalMilliseconds
)
).TotalMinutes

Related

How to add exact values to a psobject in Powershell?

I'm doing a script in powershell where i'm modifying the Chrome Bookmarks file.
This is what i want to do.
Read the file and parse the Json (Done)
Check if a certain folder is added, if it isn't, add it.
Parse again the object to Json, and save a new bookmark file. (i know how to do it)
This is how i convert it to Object:
$BkmrkJson = Get-Content $GoogleBkmrk | ConvertFrom-Json
And this is how i'm adding a new Object to the current "Childrens(Urls or bookmarks)".
$BkmrkJson.roots.bookmark_bar.children += New-Object -TypeName psobject -Property #{children=#();date_added="";date_modified="";guid="";id="";name="HV2";type="folder"}
My main problem, it's that when i add it, it isn't respecting the order of the properties. The usual order it's "children, date_added, date_modified, guid, id, name, type".
I add some values in blank, because Chrome adds new values automatically, after i add that value, or children, i parse again the psobject to Json.
$MyFinalJson = ConvertTo-Json $BkmrkJson -Depth 9
But when i create the file, it wasn't made correctly. So my principal question it's, how i can add correctly a new object to the parsed one, so when i parse it again, can recognize correctly the new ones.
hashtables (#{ ... }) are inherently unordered in PowerShell / .NET, i.e the order in which their entries are later enumerated isn't guaranteed, which means that by the time your New-Object call receives its -Property argument, the definition order of the entries in the hashtable is already lost.
However, PowerShell v3+ offers syntactic sugar for constructing custom objects ([pscustomobject] (aka [psobject])), in which case the order of entries, i.e. the order of the resulting properties is guaranteed to reflect the definition order, namely if you cast a hashtable to [pscustomobject]:
$BkmrkJson.roots.bookmark_bar.children +=
[pscustomobject] #{
children=#(); # Note: ";" only strictly needed in *single-line* defs.
date_added="";
date_modified="";
guid="";
id="";
name="Humach V2";
type="folder"
}
Note that in cases where you do want to stick with a hashtable (dictionary), you can "cast" a hashtable literal to [ordered], which also guarantees preserving the input order; specifically, this syntactic sugar creates an ordered hashtable, i.e. a System.Collections.Specialized.OrderedDictionary instance, which also implements the IDictionary interface, but (a) enumerates its entries in definition order and (b) allows accessing entries by positional index, as an alternative to using a key as the index; e.g.:
$orderedHashTable = [ordered] #{ zebra = 26; quebec = 17; alpha = 1}
$orderedHashTable.Keys # -> 'zebra', 'quebec', 'alpha'
# Access by key.
$orderedHashTable['quebec'] # -> 17
# Access by positional index
$orderedHashTable[0] # -> 26 (key 'zebra')

Getting error when adding nested hashtable to array in Powershell

I have a nested hashtable with an array and I want to loop through the contents of another array and add that to the nested hashtable. I'm trying to build a Slack message block.
Here's the nested hashtable I want to add to:
$msgdata = #{
blocks = #(
#{
type = 'section'
text = #{
type = 'mrkdwn'
text = '*Services Being Used This Month*'
}
}
#{
type = 'divider'
}
)
}
$rows = [ ['azure vm', 'centralus'], ['azure sql', 'eastus'], ['azure functions', 'centralus'], ['azure monitor', 'eastus2'] ]
$serviceitems = #()
foreach ($r in $rows) {
$servicetext = "*{0}* - {1}" -f $r[1], $r[0]
$serviceitems += #{'type'='section'}
$serviceitems += #{'text'= ''}
$serviceitems.text.Add('type'='mrkdwn')
$serviceitems.text.Add('text'=$servicetext)
$serviceitems += #{'type'='divider'}
}
$msgdata.blocks += $serviceitems
The code is partially working. The hashtables #{'type'='section'} and #{'type'='divider'} get added successfully. Trying to add the nested hashtable of #{'text' = #{ 'type'='mrkdwn' 'text'=$servicetext }} fails with this error:
Line |
24 | $serviceitems.text.Add('type'='mrkdwn')
| ~
| Missing ')' in method call.
I tried looking through various Powershell posts and couldn't find one that applies to my specific situation. I'm brand new to using hashtables in Powershell.
Complementing mklement0's helpful answer, which solves the problem with your existing code, I suggest the following refactoring, using inline hashtables:
$serviceitems = foreach ($r in $rows) {
#{
type = 'section'
text = #{
type = 'mrkdwn'
text = "*{0}* - {1}" -f $r[1], $r[0]
}
}
#{
type = 'divider'
}
}
$msgdata.blocks += $serviceitems
This looks much cleaner and thus easier to maintain in my opinion.
Explanations:
$serviceitems = foreach ... captures all output (to the success stream) of the foreach loop in variable $serviceitems. PowerShell automatically creates an array from the output, which is more efficient than manually adding to an array using the += operator. Using += PowerShell has to recreate an array of the new size for each addition, because arrays are actually of fixed size. When PowerShell automatically creates an array, it uses a more efficient data structure internally.
By writing out an inline hash table, without assigning it to a variable, PowerShell implicitly outputs the data, in effect adding it to the $serviceitems array.
We output two hash tables per loop iteration, so PowerShells adds two array elements to $serviceitems per loop iteration.
Note:
This answer addresses your question as asked, specifically its syntax problems.
For a superior solution that bypasses the original problems in favor of streamlined code, see zett42's helpful answer.
$serviceitems.text.Add('type'='mrkdwn') causes a syntax error.
Generally speaking, IF $serviceitems.text referred to a hashtable (dictionary), you need either:
method syntax with distinct, ,-separated arguments:
$serviceitems.text.Add('type', 'mrkdwn')
or index syntax (which would quietly overwrite an existing entry, if present):
$serviceitems.text['type'] = 'mrkdwn'
PowerShell even lets you access hashtable (dictionary) entries with member-access syntax (dot notation):
$serviceitems.text.type = 'mrkdwn'
In your specific case, additional considerations come into play:
You're accessing a hashtable via an array, instead of directly.
The text entry you're trying to target isn't originally a nested hashtable, so you cannot call .Add() on it; instead, you must assign a new hashtable to it.
Therefore:
# Define an empty array
$serviceItems = #()
# "Extend" the array by adding a hashtable.
# Note: Except with small arrays, growing them with +=
# should be avoided, because a *new* array must be allocated
# every time.
$serviceItems += #{ text = '' }
# Refer to the hashtable via the array's last element (-1),
# and assign a nested hashtable to it.
$serviceItems[-1].text = #{ 'type' = 'mrkdwn' }
# Output the result.
$serviceItems

LINQ in PowerShell with dynamically specified data types

In response to my previous question, I was given a working script that is based on a known-in-advance data type literally specified as [int].
Now I would like to change the data type dynamically. Is it possible?
The answer to your previous question uses type literals ([...]), which require that all types be specified verbatim (by their literal names).
Variable references are not supported in type literals; e.g. [Func[Data.DataRow, $columnType]] does not work - it causes a syntax error.
To generalize the linked answer based on dynamically determined (indirectly specified) types, two modifications are needed:
You must construct (instantiate) the closed generic types involved in the LINQ method call via the .MakeArrayType() and .MakeGenericType() methods.
You must use -as, the conditional type conversion operator to cast the input objects / the transformation script block ({ ... }) to those types.
# Create a sample data table...
[Data.DataTable] $dt = New-Object System.Data.DataTable
[Data.DataColumn] $column = New-Object System.Data.DataColumn "Id", ([int])
$dt.Columns.Add($column)
# ... and add data.
[Data.DataRow]$row = $dt.NewRow()
$row["Id"] = 1
$dt.Rows.Add($row)
$row = $dt.NewRow()
$row["Id"] = 2
$dt.Rows.Add($row)
# Using reflection, get the open definition of the relevant overload of the
# static [Linq.Enumerable]::Select() method.
# ("Open" means: its generic type parameters aren't yet bound, i.e. aren't
# yet instantiated with concrete types.)
$selectMethod = [Linq.Enumerable].GetMethods().Where({
$_.Name -eq 'Select' -and $_.GetParameters()[-1].ParameterType.Name -eq 'Func`2'
}, 'First')
# Dynamically set the name of the column to use in the projection.
$colName = 'Id'
# Dynamically set the in- and output types to use in the LINQ
# .Select() (projection) operation.
$inType = [Data.DataRow]
$outType = [int]
# Now derive the generic types required for the LINQ .Select() method
# from the types above:
# The array type to serve as the input enumerable.
# Note: As explained in the linked answer, the proper - but more cumbersome -
# solution would be to use reflection to obtain a closed instance of
# the generic .AsEnumerable() method.
$inArrayType = $inType.MakeArrayType()
# The type for the 'selector' argument, i.e. the delegate performing
# the transformation of each input object.
$closedFuncType = [Func`2].MakeGenericType($inType, $outType)
# Close the generic .Select() method with the given types
# and invoke it.
[int[]] $results = $selectMethod.MakeGenericMethod($inType, $outType).Invoke(
# No instance to operate on - the method is static.
$null,
# The arguments for the method, as an array.
# Note the use of the -as operator with the dynamically constructed types.
(
($dt.Rows -as $inArrayType),
({ $args[0].$colName } -as $closedFuncType)
)
)
# Output the result.
$results
Taking a step back:
As shown in the linked answer, PowerShell's member-access enumeration can provide the same functionality, with greatly simplified syntax and without needing to deal with types explicitly:
# Create a sample data table...
[Data.DataTable] $dt = New-Object System.Data.DataTable
[Data.DataColumn] $column = New-Object System.Data.DataColumn "Id", ([int])
$dt.Columns.Add($column)
# ... and add data.
[Data.DataRow]$row = $dt.NewRow()
$row["Id"] = 1
$dt.Rows.Add($row)
$row = $dt.NewRow()
$row["Id"] = 2
$dt.Rows.Add($row)
# Dynamically set the name of the column to use in the projection.
$colName = 'Id'
# Use member-access enumeration to extract the value of the $colName column
# from all rows.
$dt.$colName # Same as: $dt.Rows.$colName
You may want to back up and ask yourself why you are trying to use LINQ in PowerShell. A tip is that if it looks like C#, there is likely a better way to do it.
I assume that you are new to PowerShell so I will give you a quick heads up as to why LINQ is actually "easier" in PowerShell (technically it is not LINQ anymore but I think it looks like it is) when combined with the pipeline.
Try Get-Help *-Object on a PowerShell prompt sometime. Notice the cmdlets that show up? Select-Object, Where-Object, Group-Object, and Sort-Object do the same things as LINQ and their names match up to what you expect. Plus, there is no strongly-typed requirement strictly speaking.
$data | Where-Object -Property Greeting -Like *howdy* | Select-Object Name,Year,Greeting

Powershell only outputting first character

I have the following dataset:
id|selectedquery|
1|SELECT fieldX FROM tableA|
2|SELECT fieldY FROM tableB|
that dataset is used in the following code
$rows=($dataSet.Tables | Select-Object -Expand Rows)
$i=0
foreach ($row in $rows)
{
#Write-Output $rows.selectquery[$i].length
$query = $rows.selectquery[$i]
#Write-Output $rows.selectquery[$i]
--doing some stuff--
$i++
}
Often $rows.selectquery[$i] only gives me the first character of the value in the field selectedquery being the 'S'.
When I remove the [$i] from $rows.selectquery it gives me (understandably) multiple records back. If I then put the [$i] back after $rows.selectquery[$i] things woerk fine.
Can anyone explain this behaviour?
You'll want to reference the SelectQuery property on either $row or $rows[$i] - not the entire $rows collection:
$row.SelectQuery
# or
$rows[$i].SelectQuery
Mathias' helpful answer shows the best way to solve your particular problem.
As for what happened:
You - inadvertently - used PowerShell's member-access enumeration feature when you used $rows.selectquery; that is, even though $rows is a collection that itself has no .selectquery property, PowerShell accessed that property on every element of the collection and returned the resulting values as an array.
The pitfall is that if the collection only has one element, the return value is not an array - it is just the one and only element's property value itself.
While this is analogous to how the pipeline operates (a single output object is captured by itself if assigned to a variable, while two or more are implicitly collected in an array), it is somewhat counterintuitive in the context of member-access enumeration:
In other words, $collection.SomeProperty is equivalent to $collection | ForEach-Object { $_.SomeProperty } and not, as would make more sense, because it always returns an array (collection), $collection.ForEach('SomeProperty')
GitHub issue #6802 discusses this problem.
While this behavior is often unproblematic, because PowerShell offers unified handling of scalars and collections (e.g. (42)[0], is the same as 42 itself; see this answer), a problem arises if the single value returned happens to be a string, because indexing into a string returns its characters.
Workaround: Cast to [array] before applying the index:
([array] $rows.selectquery)[0]
A simple example:
# Multi-element array.
[array] $rows1 = [pscustomobject] #{ selectquery = 'foo' },
[pscustomobject] #{ selectquery = 'bar' }
# Single-element array:
[array] $rows2 = [pscustomobject] #{ selectquery = 'baz' }
# Contrast member-access enumeration + index access between the two:
[pscustomobject] #{
MultiElement = $rows1.selectquery[0]
SingleElement = $rows2.selectquery[0]
SinglElementWithWorkaround = ([array] $rows2.selectquery)[0]
}
The above yields the following:
MultiElement SingleElement SinglElementWithWorkaround
------------ ------------- --------------------------
foo b baz
As you can see, the multi-element array worked as expected, because the member-access enumeration returned an array too, while the single-element array resulted in single string 'baz' being returned and 'baz'[0] returns its first character, 'b'.
Casting to [array] first avoids that problem (([array] $rows2.selectquery)[0]).
Using #(...), the array-subexpression operator - #($rows.selectquery)[0] - is another option, but, for the sake of efficiency, it should only be used on commands (e.g., #(Get-ChildItem -Name *.txt)[0]) not expressions, as in the case at hand.)

Create array from keys of an array of hashtables

I have an array where each element is a hashtable. Each hashtable has the same keys. Here it is:
#(
#{"MarketShortCode"="abc";"MarketName"="Market1" },
#{"MarketShortCode"="def";"MarketName"="Market2" },
#{"MarketShortCode"="ghi";"MarketName"="Market3" },
#{"MarketShortCode"="jkl";"MarketName"="Market4" }
)
I want a nice elegant way to extract an array containing just the value of the MarketShortCode key. So I want this:
#("abc","def","ghi","jkl")
This is the best I've come up with:
$arr = #()
$hash | %{$arr += $_.MarketShortCode}
$arr
But I don't like that cos its three lines of code. Feels like something I should be able to do in one line of code. Is there a way?
Just do this:
$hash | %{$_.MarketShortCode}
That is, return the value from the block instead of adding it to an array and then dereferencing the array.
If you're using PowerShell 3+, there's even shorter way:
$hash.MarketShortCode
PowerShell automatically applies dot . to each item in an array when it's used this way, but it wasn't supported until v3.