Sort and delete duplicates in multidimensional array - powershell

I have an multidimensional Array and want to sort it by the date and delete the duplicate entries (by date).
$arrayPlaces = [System.Collections.ArrayList]#()
and that is how the Array looks like:
$arrayPlaces.Add(($mailBarcodeInformation[2], $mailScannedStart.Substring(22), "", "", "", "", "", ""))
The empty fields are filled later. The second field $mailScannedStart.Substring(22) is the time and the Array should be sorted by that and the duplicates should also be removed.
I searched a lot but couldn't find any help.

A common approach is to add a custom object into an array and use the buit-in cmdlet sort -unique. It's much more readable:
$arrayPlaces = [System.Collections.ArrayList]#()
#.........
$arrayPlaces.Add([PSCustomObject]#{
barcode = $mailBarcodeInformation[2]
time = $mailScannedStart.Substring(22)
foo1 = ''
foo2 = ''
foo3 = ''
}) >$null
#...........
$sorted = $arrayPlaces | sort time -Unique
However, since you already use a .NET class you can switch to a fast .NET SortedDictionary, which automatically sorts and deduplicates the collection when items are added:
$arrayPlaces = [Collections.Generic.SortedDictionary[string,array]]#{}
Adding and overwriting the old value:
$key = $mailScannedStart.Substring(22)
$arrayPlaces[$key] = $mailBarcodeInformation[2], $key, "", "", "", "", "", ""
Checking for presence of an item using its key:
if ($arrayPlaces.ContainsKey($key)) { ....... }
Removing:
[void]$arrayPlaces.Remove('foo')
Accessing:
$items = $arrayPlaces['foo']
$item = $arrayPlaces['foo'][0]
Enumerating (faster):
foreach ($items in $arrayPlaces.Values) {
# .........
}
Enumerating/pipelining (slower):
$arrayPlaces.Values | foreach { $_[0] = $_[1] + $[2] }

Related

Perl get all values from hahes inside of array

I`m really struggling with Perl and I need to solve this topic. I Have a rest api response that I converted to json, Dumper shows something like this:
VAR1= [
{
"id":"abc",
"type":"info",
"profile":
{"name":"Adam",
"description":"Adam description"}
},
{
"id":"efg",
"type":"info",
"profile":
{"name":"Jean",
"description":"Jean description"}
},
{
"id":"hjk",
"type":"info",
"profile":
{"name":"Jack",
"description":"Jack description"}
},
]
What I need is to iterate over each "name" and check if value is Jean. I wanted to iterate over hashes inside of array, but each time it will only store first hash, not all of them.
What I`m trying and failing:
# my json gather, Dumper is shown above.
my $result_json = JSON::from_json($rest->GET( $host, $headers )->{_res}->decoded_content);
# I`ve tried many things to get all hashes, but either error, or single hash, or single value:
my $list = $result_json->[0];
my $list2 = $result_json->[0]->{'profile'};
my $list3 = #result_json->[0];
my $list4 = #result_json->[0]->{'profile'};
my $list5 = #result_json;
my $list5 = #result_json->{'profile'}; # this throws error
my $list6 = #result_json->[0]->{'profile'}->{'name'};
my $list7 = $result_json->[0]->{'profile'}->{'name'};
# and maybe more combinations... its just an example.
foreach my $i (<lists above>){
print $i;
};
Any idea how to set it up properly and iterate over each "name"?
Assuming that the call to JSON::from_json shown in the code smaple is indeed given the JSON string shown as a Dumper output,† that $result_json is an array reference so iterate over its elements (hash references)
foreach my $hr (#{ $result_json }) {
say "profile name: ", $hr->{profile}{name};
}
† That last comma in the supposed Dumper's output can't actually be there, so I removed it to use the rest as a sample JSON for testing

Split PSCustomObjects in to strings/array

Hi I have n sum of objects for each Node like this:
NodeName : 11111
System.AreaId : 2375
System.AreaPath : Project
System.TeamProject : Project
System.NodeName : Project
System.AreaLevel1 : Project
Every node can have different objects in it.
How can I split them to an arrays/strings without specifying the object name so I can create foreach separate object loop?
mklement0 beat me to what I was going to post. Since I have the code drafted already I will post it.
Like mklement0 said in comments, you can access object properties through use of .psobject.Properties. Below in the code I am using a switch statement to check if an object contains a specific property.
$objs = #(
[pscustomobject]#{
AreaId = 2375
AreaPath = ''
TeamProject = 'Project2'
NodeName = ''
AreaLevel1 = ''
},
[pscustomobject]#{
AreaId = 342
AreaPath = ''
TeamProject = 'Project2'
Color = 'Red'
}
)
switch ($objs) {
{ $_.psobject.properties.name -contains 'Color' } {
'Object contains Color property'
}
{ $_.psobject.properties.name -contains 'NodeName' } {
'Object contains NodeName property'
}
Default {}
}

Is it possible to create specific constant elements of a PowerShell array?

Let's say I have array Grid that was initialized with
$Grid = #(#(1..3), #(4..6), #(7..9))
and I want to change Grid[0][0] to value "Test" but I want to make it unchangeable, is there a way I could to that?
So far I've tried playing around with classes that allow read-only or constant declarations through the class as opposed to using New-Variable/Set-Variable but it doesn't affect the index itself but the individual element as in
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0] # 1 \n 2 \n 3
$Grid[0][0].IsReadOnly # True
$Grid[0][0] = "test"
$Grid[0][0] # test
I assume this is due to $Grid[0][0] being read-only as opposed to constant and the behaviour I experienced supported that:
$test = [Array]::AsReadOnly(#(1,2,3,4))
$test[0]=1 # Errors
$test = "test"
$test # test
$Grid = #(#(1..3), #(4..6), #(7..9))
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0][0] = 1 # Errors
$Grid[0][0] = "test"
$Grid[0][0] # test
I'm not sure what to try next and I know that this is very simple with classes but I am not looking for that as a solution.
You'll have to make both dimensions of your nested array read-only to prevent anyone from overwriting $grid[0]:
$grid =
[array]::AsReadOnly(#(
,[array]::AsReadOnly(#(1,2,3))
,[array]::AsReadOnly(#(3,2,1))
))
(the unary , is not a typo, it prevents PowerShell from "flattening" the resulting read-only collection)
Now $grid should behave as you expect:
$grid[0] # 1,2,3
$grid[0][0] # 1
$grid[0][0] = 4 # error
$grid[0] = 4 # error
If you want to be able to prevent writing to individual "cells", you'll have to define a custom type:
using namespace System.Collections
class Grid {
hidden [int[,]] $data
hidden [bool[,]] $mask
Grid([int]$width,[int]$height){
$this.mask = [bool[,]]::new($width, $height)
$this.data = [int[,]]::new($width, $height)
}
[int]
Get([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
return $this.data[$x,$y]
}
Set([int]$x,[int]$y,[int]$value)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
if(-not $this.mask[$x,$y])
{
$this.data[$x,$y] = $value
}
else
{
throw [System.InvalidOperationException]::new("Cell [$x,$y] is currently frozen")
}
}
Freeze([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $true
}
Unfreeze([int]$x,$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $false
}
hidden [bool]
CheckBounds([int]$x,[int]$y)
{
return (
$x -ge $this.data.GetLowerBound(0) -and
$x -le $this.data.GetUpperBound(0) -and
$y -ge $this.data.GetLowerBound(1) -and
$y -le $this.data.GetUpperBound(1)
)
}
}
Now you can do:
$grid = [Grid]::new(5,5)
$grid.Set(0, 0, 1) # Set cell value
$grid.Get(0, 0) # Get cell value
$grid.Freeze(0, 0) # Now freeze cell 0,0
$grid.Set(0, 0, 2) # ... and this will now throw an exception
$grid.Set(0, 1, 1) # Setting any other cell still works
If you want native support for index expressions (ie. $grid[0,0]), the Grid class will need to implement System.Collections.IList

How to convert a string value to a dynamic data type in PowerShell?

Is it possible to assign a string value to a variable of a different type given that the data type is not known in advance? For example, in the sample below, how do I update the values of the $values hash without changing their data types:
$values = #{
"Boolean" = $true
"Int" = 5
"DateTime"= (Get-Date)
"Array" = #("A", "B", "C")
}
$stringValues = #{
"Boolean" = 'false'
"Int" = '10'
"DateTime"= '2019-01-02 14:45:59.146'
"Array" = '#("X", "Y", "Z")'
}
"INITIAL VALUES:"
foreach ($key in $values.Keys) {
($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}
"`nUPDATING..."
foreach ($key in $stringValues.Keys) {
$values[$key] = $stringValues[$key]
}
"`nUPDATED VALUES:"
foreach ($key in $values.Keys) {
($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}
OUTPUT:
INITIAL VALUES:
DateTime = 04/23/2019 16:54:13 (System.DateTime)
Array = A B C (System.Object[])
Boolean = True (System.Boolean)
Int = 5 (System.Int32)
UPDATING...
UPDATED VALUES:
DateTime = 2019-01-02 14:45:59.146 (System.String)
Array = #("X", "Y", "Z") (System.String)
Boolean = false (System.String)
Int = 10 (System.String)
I need the updated values to match the original data types and not just get converted to System.String.
I am flexible on the contents of the strings. E.g. a string holding a boolean false value may be $false/false/[boolean]false/[boolean]$false/etc or a string holding an array may use a different formatting (basically, whatever is easier to convert the string to a proper data type).
In essence, I want to simulate whatever the ConvertFrom-Json cmdlet does when it sets the object property from a JSON string, only in my case, I do not have a JSON structure.
(In case someone wonders what I'm trying to do: I am trying to add an INI file parser to my ConfigFile module, and no, I cannot just use a hash to return the INI settings; I need to load the values into the corresponding PSVariables and for this to work, I need to convert strings to proper data types.)
So you want to cast/convert the new value to the type of the old value.
The idea needs to cast from a variable,
here is a related question powershell-type-cast-using-type-stored-in-variable
The answer suggest:
You can roughly emulate a cast using the following method:
[System.Management.Automation.LanguagePrimitives]::ConvertTo($Value, $TargetType)
The following changed routine shows: it isn't that simple, especially when the new data needs overloads/other parameters in the conversion.
"UPDATING..."
foreach ($key in $stringValues.Keys) {
$values[$key] = [System.Management.Automation.LanguagePrimitives]::ConvertTo(
$stringValues[$key], $values[$key].gettype())
}
My German locale error message:
Ausnahme beim Aufrufen von "ConvertTo" mit 2 Argument(en): "Der Wert "2019-01-02 14:45.59.146" kann nicht in den Typ
"System.DateTime" konvertiert werden. Fehler: "Die Zeichenfolge wurde nicht als gültiges DateTime erkannt.""
In Zeile:2 Zeichen:5
+ $values[$key] = [System.Management.Automation.LanguagePrimitives] ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : PSInvalidCastException
And the unsufficient result:
DateTime = 04/24/2019 09:49:19 (System.DateTime)
Array = #("X", "Y", "Z") (System.Object[])
Boolean = True (System.Boolean)
Int = 10 (System.Int32)
You may elaborate yourself on this idea, handling old types/new data more individually.
You can use a custom class in lieu of a hashtable; unlike hashtable keys, the properties of custom classes can be specifically typed.
With scalar values, you can then simply let PowerShell perform the from-string conversion for you - except that Boolean strings need special treatment (see comments in source code below).
With arrays, things are trickier. The solution below uses [System.Management.Automation.PSParser]::Tokenize() to parse the string, but is currently limited to recognizing string and number literals.
Note: It is tempting to use Invoke-Expression on the entire array, but that would be a security risk, because it opens to the door to arbitrary code execution. While there are legitimate uses - such as on a string known to represent a number below - Invoke-Expression should generally be avoided.
(If you don't want to define classes, you can derive the types from the values of hashtable $values and use [System.Management.Automation.LanguagePrimitives]::ConvertTo() to convert the strings to those types, as shown in LotPings' answer, though note that arrays and Booleans still need special treatment as shown below.)
# Define a custom [Values] class
# with specifically typed properties.
class Values {
[bool] $Boolean
[int] $Int
[datetime] $DateTime
[Array] $Array
}
# Instantiate a [Values] instance
$values = [Values] #{
Boolean = $true
Int = 5
DateTime= (Get-Date)
Array = #("A", "B", "C")
}
$stringValues = #{
Boolean = 'false'
Int = '10'
DateTime = '2019-01-02 14:45:59.146'
Array = '#("X", "Y", "Z")'
}
"INITIAL VALUES:"
foreach ($key in $values.psobject.properties.Name) {
($key + " = " + $values.$key + " (" + $values.$key.GetType().FullName + ")")
}
""
"UPDATING..."
foreach ($key in $stringValues.Keys) {
switch ($key) {
'Array' {
# Parse the string representation.
# Assumptions and limitations:
# The array is flat.
# It is sufficient to only support string and numeric constants.
# No true syntax validation is needed.
$values.$key = switch ([System.Management.Automation.PSParser]::Tokenize($stringValues[$key], [ref] $null).Where( { $_.Type -in 'String', 'Number' })) {
{ $_.Type -eq 'String' } { $_.Content; continue }
{ $_.Type -eq 'Number' } { Invoke-Expression $_Content; continue }
}
continue
}
'Boolean' { # Boolean scalar
# Boolean strings need special treatment, because PowerShell considers
# any nonempty string $true
$values.$key = $stringValues[$key] -notin 'false', '$false'
continue
}
default { # Non-Boolean scalar
# Let PowerShell perform automatic from-string conversion
# based on the type of the [Values] class' target property.
$values.$key = $stringValues[$key]
}
}
}
""
"UPDATED VALUES:"
foreach ($key in $values.psobject.properties.Name) {
($key + " = " + $values.$key + " (" + $values.$key.GetType().FullName + ")")
}
This yields:
INITIAL VALUES:
Boolean = True (System.Boolean)
Int = 5 (System.Int32)
DateTime = 04/24/2019 14:45:29 (System.DateTime)
Array = A B C (System.Object[])
UPDATING...
UPDATED VALUES:
Boolean = True (System.Boolean)
Int = 10 (System.Int32)
DateTime = 01/02/2019 14:45:59 (System.DateTime)
Array = X Y Z (System.Object[])
Agreed on the Write-Host thing. It should really only be used to leverage color output and some specific format cases. Output to the screen is the default as you'll see in my response.
You could do the below, but that date string is a bit odd, well, for me, well, I've not seen anyone use that format. So, I modified it for US style, but change as needed for your language.
$values = #{
'Boolean' = $true
'Int' = 5
'DateTime'= (Get-Date)
'Array' = #('A', 'B', 'C')
}
$stringValues = #{
'Boolean' = 'false'
'Int' = '10'
'DateTime'= '2019-01-02 14:45:59'
'Array' = "#('X', 'Y', 'Z')"
}
'INITIAL VALUES:'
foreach ($key in $values.Keys)
{
"$key = $($values[$key]) $($values[$key].GetType())"
}
"`nUPDATING..."
foreach ($key in $stringValues.Keys)
{
switch ($key)
{
Boolean {[Boolean]$values[$key] = $stringValues['$'+$key]}
Int {[Int]$values[$key] = $stringValues[$key]}
DateTime {[DateTime]$values[$key] = $stringValues[$key]}
Array {[Array]$values[$key] = $stringValues[$key]}
default {'The value could not be determined.'}
}
}
"`nUPDATED VALUES:"
foreach ($key in $values.Keys)
{
"$key = $($values[$key]) $($values[$key].GetType())"
}
# Results
INITIAL VALUES:
DateTime = 04/24/2019 01:44:17 datetime
Array = A B C System.Object[]
Boolean = True bool
Int = 5 int
UPDATING...
UPDATED VALUES:
DateTime = 01/02/2019 14:45:59 datetime
Array = #("X", "Y", "Z") System.Object[]
Boolean = False bool
Int = 10 int
Thanks to #LotPings, #mklement0, and #postanote for giving me a few ideas, so here is the solution I will go with:
$values = #{
"Boolean" = $true
"Int" = 5
"DateTime"= (Get-Date)
"Array" = #("A", "B", "C")
}
$stringValues = #{
"Boolean" = 'false'
"Int" = '10'
"DateTime"= '2019-01-31 14:45:59.005'
"Array" = 'X,Y,Z'
}
"INITIAL VALUES:"
foreach ($key in $values.Keys) {
($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}
"`nUPDATING..."
foreach ($key in $stringValues.Keys) {
$value = $stringValues[$key]
if ($values[$key] -is [Array]) {
$values[$key] = $value -split ','
}
elseif (($values[$key] -is [Boolean]) -or ($values[$key] -is [Switch])) {
$values[$key] = $value -notin 'false', '$false', '0', ''
}
else {
$values[$key] = [System.Management.Automation.LanguagePrimitives]::ConvertTo($value, $values[$key].GetType())
}
}
"`nUPDATED VALUES:"
foreach ($key in $values.Keys) {
($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}
OUTPUT:
INITIAL VALUES:
DateTime = 04/25/2019 09:32:31 (System.DateTime)
Array = A B C (System.Object[])
Boolean = True (System.Boolean)
Int = 5 (System.Int32)
UPDATING...
UPDATED VALUES:
DateTime = 01/31/2019 14:45:59 (System.DateTime)
Array = X Y Z (System.String[])
Boolean = False (System.Boolean)
Int = 10 (System.Int32)
I adjusted the format of the array in the string value (which, as I mentioned in the question, was an option). The actual code that will use this will be a bit different, but the basic idea is here. The only caveat that I noticed is that the array data type gets changed from object[] to string[]. Ideally, I'd like to keep it as-is, but it would not change the functionality of the code, so it is fine. Thanks again to all for the ideas and corrections, and if you come up with better alternatives, feel free to post.

OR condition inside AND condition in createQueryBuilder depend on condition

We need all active records using "tag" which matches search string in createQueryBuilder.
For Ex: Search for "NEW YORK" which internally search for "NEW" or "YORK" in Tags "new, ab, other"
My CODE:
$query = $this->createQueryBuilder();
$query->field('status')->equals("active");
if (isset($params["search"]) && !empty($params["search"])) {
$searchArr = explode(" ", $params["search"]);
foreach ($searchArr as $searchVal) {
$query->addOr(
$query->expr()->addOr($query->expr()->field('tags')->equals(new \MongoRegex('/.*' . $searchVal . '.*/')))
);
}
}
It will give result active OR (tag1 Or tag2). Expected result active AND (tag1 Or tag2)
Any suggestions?
Replace this
foreach ($searchArr as $searchVal) {
$query->addOr(
by this
foreach ($searchArr as $searchVal) {
$query->addAnd(