I'm reading a tutorial and learned that PowerShell supports ordered hashes. When would I use that feature?
Sample code of what I'm talking about:
$hash = [ordered]#{ ID = 1; Shape = "Square"; Color = "Blue"}
Let me complement Maximilian Burszley's helpful answer with a broader perspective:
tl;dr
Most of the time you want [ordered] #{ ... } ([System.Collections.Specialized.OrderedDictionary]) (PSv3+):
It provides enumeration of the entries in the order in which they were defined (also reflected in the .Keys and .Values collection properties).
It also allows accessing entries by index, like an array.
Typically, you can use [ordered] #{ ... } interchangeably with #{ ... }, the regular hashtable, [hashtable] a.k.a [System.Collections.Hashtable], because both types implement the [IDictionary] interface, which is how parameters that accept hash tables are typically typed.
The performance penalty you pay for using [ordered] is negligible.
Some background:
For technical reasons, the most efficient implementation of a hashtable (hash table) is to let the ordering of entries be the outcome of implementation details, without guaranteeing any particular order to the caller.
This is fine for use cases where all you do is to perform isolated lookups by key, where the ordering among keys (entries) is irrelevant.
However, often you do care about the ordering of entries:
in the simplest case, for display purposes; there is something disconcerting about seeing the definition order jumbled; e.g.:
#{ one = 1; two = 2; three = 3 }
Name Value
---- -----
one 1
three 3 # !!
two 2
more importantly, the enumeration of entries may need to be predictable for further programmatic processing; e.g. (note: strictly speaking, property order doesn't matter in JSON, but it is again important for the human observer):
# No guaranteed property order.
PS> #{ one = 1; two = 2; three = 3 } | ConvertTo-Json
{
"one": 1,
"three": 3, # !!
"two": 2
}
# Guaranteed property order.
PS> [ordered] #{ one = 1; two = 2; three = 3 } | ConvertTo-Json
{
"one": 1,
"two": 2,
"three": 3
}
It's unfortunate that PowerShell's hashtable-literal syntax, #{ ... }, doesn't default to [ordered][1], but it is too late to change that.
There is one context in which [ordered] is implied, however: if you cast a hashtable literal to [pscustomobject] in order to create a custom object:
[pscustomobject] #{ ... } is syntactic sugar for [pscustomobject] [ordered] #{ ... }; that is, the resulting custom object's properties are ordered based on the entry order in the hashtable literal; e.g.:
PS> [pscustomobject] #{ one = 1; two = 2; three = 3 }
one two three # order preserved!
--- --- -----
1 2 3
Note, however, that this only works exactly as shown above: if the cast applied directly to a hashtable literal; if you use a variable to store the hashtable in first or if you even just enclose the literal in (...) the ordering is lost:
PS> $ht = #{ one = 1; two = 2; three = 3 }; [pscustomobject] $ht
one three two # !! Order not preserved.
--- ----- ---
1 3 2
PS> [pscustomobject] (#{ one = 1; two = 2; three = 3 }) # Note the (...)
one three two # !! Order not preserved.
--- ----- ---
1 3 2
Therefore, if you construct a hashtable iteratively first and then cast it to [pscustomobject], you must start with an [ordered] hashtable to get predictable ordering of properties; this technique is useful, because it's easier to create hashtable entries than it is to add properties to a custom object; e.g.:
$oht = [ordered] #{} # Start with an empty *ordered* hashtable
# Add entries iteratively.
$i = 0
foreach ($name in 'one', 'two', 'three') {
$oht[$name] = ++$i
}
[pscustomobject] $oht # Convert the ordered hashtable to a custom object
Finally, note that [ordered] can only be applied to hashtable literal; you cannot use it to convert a preexisting regular hashtable to an ordered one (which wouldn't make any sense anyway, because you have no defined order to begin with):
PS> $ht = #{ one = 1; two = 2; three = 3 }; [ordered] $ht # !! Error
...
The ordered attribute can be specified only on a hash literal node.
...
On a side note: Neither ordered nor regular hashtables enumerate their entries when sent through the pipeline; they are sent as a whole.
To enumerate the entries, use the .GetEnumerator() method; e.g.:
#{ one = 1; two = 2; three = 3 }.GetEnumerator() | ForEach-Object { $_.Value }
1
3 # !!
2
As for the performance impact of using [ordered]:
As noted, it is negligible; here are some sample timings, averaged across 10,000 runs, using Time-Command:
Time-Command -Count 10,000 { $ht=#{one=1;two=2;three=3;four=4;five=5;six=6;seven=7;eight=8;nine=9}; foreach($k in $ht.Keys){$ht.$k} },
{ $ht=[ordered] #{one=1;two=2;three=3;four=4;five=5;six=6;seven=7;eight=8;nine=9}; foreach($k in $ht.Keys){$ht.$k} }
Sample timings (Windows PowerShell 5.1 on Windows 10, single-core VM):
Command TimeSpan Factor
------- -------- ------
$ht=#{one=1;two=2;th... 00:00:00.0000501 1.00
$ht=[ordered] #{one=... 00:00:00.0000527 1.05
That is, [ordered] amounted to a mere 5% slowdown.
[1] Maximilian Burszley points out one tricky aspect specific to [ordered] hashtables:
With numeric keys, distinguishing between a key and an index can become tricky; to force interpretation of a number as a key, cast it to [object] or use dot notation (., property-access syntax) instead of index syntax ([...]):
# Ordered hashtable with numeric keys.
PS> $oht = [ordered] #{ 1 = 'one'; 2 = 'two' }
PS> $oht[1] # interpreted as *index* -> 2nd entry
two
PS> $oht[[object] 1] # interpreted as *key* -> 1st entry.
one
PS> $oht.1 # dot notation - interpreted as *key* -> 1st entry.
one
That said, numeric keys aren't common, and to me the benefit of defaulting to predictable enumeration outweighs this minor problem.
The .NET type underlying [ordered], System.Collections.Specialized.OrderedDictionary, has been available since v1, so PowerShell could have chosen it as the default implementation for #{ ... } from the get-go, even in PowerShell v1.
Given PowerShell's commitment to backward compatibility, changing the default is no longer an option, however, as that could break existing code, namely in the following ways:
There may be existing code that checks untyped arguments for whether they're a hashtable with -is [hashtable], which would no longer work with an ordered hashtable (however, checking with -is [System.Collections.IDictionary] would work).
There may be existing code that relies on hashtables with numeric keys, in which case the index-syntax lookup behavior would change (see example above).
The reason for an ordered dictionary is for display / typecast purposes. For example, if you want to cast your hashtable to a PSCustomObject and you want your keys to be in the order you enter them, you use ordered.
The use case here is when you use Export-Csv, the headers are in the right order. This is just one example I could think of off the top of my head. By design, the hashtable type doesn't care about the order you enter keys/values and will be different each time you display it to the success stream.
An additional use-case for the ordered dictionary: you can treat your hashtable as an array and use numerical accessors to find items, such as $myOrderedHash[-1] will grab the last item added to the dictionary.
Related
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')
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
Can an array be used as the key in a hashtable? How can I reference the hashtable item with an array key?
PS C:\> $h = #{}
PS C:\> $h[#(1,2)] = 'a'
PS C:\> $h
Name Value
---- -----
{1, 2} a # looks like the key is a hash
PS C:\> $h[#(1,2)] # no hash entry
PS C:\> $h.Keys #
1
2
PS C:\> $h[#(1,2)] -eq 'a'
PS C:\> $h[#(1,2)] -eq 'b'
PS C:\> foreach ($key in $h.Keys) { $key.GetType() } # this is promising
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS C:\> $PSVersionTable.PSVersion.ToString()
7.1.4
While you can use arrays as hashtable keys, doing so is impractical:
Update: There is a way to make arrays work as hashtable keys, but it requires nontrivial effort during construction of the hashtable - see this answer.
You'll have the use the very same array instances as both the keys and for later lookups.
The reason is that arrays, which are instances of .NET reference types (as opposed to value types such as integers), use the default implementation of the .GetHashCode() method to return a hash code (as used in hashtables), and this default implementation returns a different code for each instance - even for two array instances that one would intuitively think of as "the same".
In other words: you'll run into the same problem trying to use instances of any such .NET reference type as hashtable keys, including other collection types - unless a given type happens to have a custom .GetHashCode() implementation that explicitly considers distinct instances equal based on their content.
Additionally, it makes use of PowerShell's indexer syntax ([...]) awkward, because the array instance must be nested, with the unary form of ,, the array constructor operator. However, dot notation (property access) works as usual.
$h = #{}
# The array-valued key.
$key = 1, 2
$h[$key] = 'a'
# IMPORTANT:
# The following lookups work, but only because
# the *very same array instance* is used for the lookup.
# Nesting required so that PowerShell doesn't think that
# *multiple* keys are being looked up.
$h[, $key]
# Dot notation works normally.
$h.$key
# Does NOT work, because a *different array instance* is used.
$h.#(1,2)
A simple test for whether a given expression results in the same hashtable lookup every time and is therefore suitable as a key is to call the .GetHashCode() method on it repeatedly; only if the same number is returned every time (in a given session) can the expression be used:
# Returns *different* numbers.
#(1, 2).GetHashCode()
#(1, 2).GetHashCode()
To inspect a given object or type for whether it is (an instance of) a .NET reference type vs. value type:
# $false is returned in both cases, confirming that the .NET array
# type is a *reference type*
#(1, 2).GetType().IsValueType
[Array].IsValueType
Workaround:
A workaround would be to use string representations of arrays, though coming up with unique (enough) ones may be a challenge.
In the simplest case, use PowerShell's string interpolation, which represents arrays as a space-separated list of the elements' (stringified) values; e.g. "$(1, 2)" yields verbatim 1 2:
$h = #{}
# The array to base the key on.
$array = 1, 2
# Use the *stringified* version as the key.
$h["$array"] = 'a'
# Works, because even different array instances with equal-valued
# instances of .NET primitive types stringify the same.
# '1 2'
$h["$(1, 2)"]
iRon points out that this simplistic approach can lead to ambiguity (e.g., a single '1 2' string would result in the same key as array 1, 2) and recommends the following instead:
a more advanced/explicit way for array keys would be:
joining their elements with a non-printable character; e.g.
$key = $array -join [char]27
or, for complex object array elements, serializing the array:
$key = [System.Management.Automation.PSSerializer]::Serialize($array)
Note that even the XML (string)-based serialization provided by the System.Management.Automation.PSSerializer class (used in PowerShell remoting and background jobs for cross-process marshaling) has its limits with respect to reliably distinguishing instances, because its recursion depth is limited - see this answer for more information; you can increase the depth on demand, but doing so can result in very large string representations.
A concrete example:
using namespace System.Management.Automation
$ht = #{}
# Use serialization on an array-valued key.
$ht[[PSSerializer]::Serialize(#(1, 2))] = 'a'
# Despite using a different array instance, this
# lookup succeeds, because the serialized representation is the same.
$ht[[PSSerializer]::Serialize(#(1, 2))] # -> 'a'
The primary cause of your problems here is that PowerShell's index access operator [] supports multi-index access by enumerating any array values passed.
To understand why, let's have a look at how the index accessor [...] actually works in PowerShell. Let's start with a simple hashtable, with 2 entries using scalar keys:
$ht = #{}
$ht['a'] = 'This is value A'
$ht['b'] = 'This is value B'
Now, let's inspect how it behaves!
Passing a scalar argument resolves to the value associated with the key represented by said argument, so far so good:
PS ~> $ht['a']
This is value A
But we can also pass an array argument, and all of a sudden PowerShell will try to resolve all items as individual keys:
PS ~> $ht[#('a', 'b')]
This is value A
This is value B
PS ~> $ht[#('b', 'a')] # let's try in reverse!
This is value B
This is value A
Now, to understand what happens in your example, let's try an add an entry with an array reference as the key, along with two other entries where the key is the individual values fround in the array:
$ht = #{}
$keys = 1,2
$ht[$keys[0]] = 'Value 1'
$ht[$keys[1]] = 'Value 2'
$ht[$keys] = 'Value 1,2'
And when we subsequently try to resolve the last entry using our array reference:
PS ~> $ht[$keys]
Value 1
Value 2
Oops! PowerShell unraveled the $keys array, and never actually attempted to resolve the entry associated with the key corresponding to the array reference in $keys.
In other words: The index accessor cannot be used to resolve dictionary entries by key is the key type is enumerable
So, how does one access an entry by array reference without having PowerShell unravel the array?
Use the IList.Item() parameterized property instead:
PS ~> $ht.Item($keys)
Value 1,2
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.)
I am having a little bit of trouble with hashtables/dictionaries in powershell. The most recent roadblock is the ability to find the index of a key in an ordered dictionary.
I am looking for a solution that isn't simply iterating through the object.
(I already know how to do that)
Consider the following example:
$dictionary = [Ordered]#{
'a' = 'blue';
'b'='green';
'c'='red'
}
If this were a normal array I'd be able to look up the index of an entry by using IndexOf().
[array]::IndexOf($dictionary,'c').
That would return 2 under normal circumstances.
If I try that with an ordered dictionary, though, I get -1.
Any solutions?
Edit:
In case anyone reading over this is wondering what I'm talking about. What I was trying to use this for was to create an object to normalize property entries in a way that also has a numerical order.
I was trying to use this for the status of a process, for example:
$_processState = [Ordered]#{
'error' = 'error'
'none' = 'none'
'started' = 'started'
'paused' = 'paused'
'cleanup' = 'cleanup'
'complete' = 'complete'
}
If you were able to easily do this, the above object would give $_processState.error an index value of 0 and ascend through each entry, finally giving $_processState.complete an index value of 5. Then if you compared two properties, by "index value", you could see which one is further along by simple operators. For instance:
$thisObject.Status = $_processState.complete
If ($thisObject.Status -ge $_processState.cleanup) {Write-Host 'All done!'}
PS > All done!
^^that doesn't work as is, but that's the idea. It's what I was aiming for. Or maybe to find something like $_processState.complete.IndexNumber()
Having an object like this also lets you assign values by the index name, itself, while standardizing the options...
$thisObject.Status = $_processState.paused
$thisObject.Status
PS > paused
Not really sure this was the best approach at the time or if it still is the best approach with all the custom class options there are available in PS v5.
It can be simpler
It may not be any more efficient than the answer from Frode F., but perhaps more concise (inline) would be simply putting the hash table's keys collection in a sub expression ($()) then calling indexOf on the result.
For your hash table...
Your particular expression would be simply:
$($dictionary.keys).indexOf('c')
...which gives the value 2 as you expected. This also works just as well on a regular hashtable... unless the hashtable is modified in pretty much any way, of course... so it's probably not very useful in that case.
In other words
Using this hash table (which also shows many of the ways to encode 4...):
$hashtable = [ordered]#{
sample = 'hash table'
0 = 'hello'
1 = 'goodbye'
[char]'4' = 'the ansi character 4 (code 52)'
[char]4 = 'the ansi character code 4'
[int]4 = 'the integer 4'
'4' = 'a string containing only the character 4'
5 = "nothing of importance"
}
would yield the following expression/results pairs:
# Expression Result
#------------------------------------- -------------
$($hashtable.keys).indexof('5') -1
$($hashtable.keys).indexof(5) 7
$($hashtable.keys).indexof('4') 6
$($hashtable.keys).indexof([char]4) 4
$($hashtable.keys).indexof([int]4) 5
$($hashtable.keys).indexof([char]'4') 3
$($hashtable.keys).indexof([int][char]'4') -1
$($hashtable.keys).indexof('sample') 0
by the way:
[int][char]'4' equals [int]52
[char]'4' has a "value" (magnitude?) of 52, but is a character, so it's used as such
...gotta love the typing system, which, while flexible, can get really really bad at times, if you're not careful.
Dictionaries uses keys and not indexes. OrderedDictionary combines a hashtable and ArrayList to give you order/index-support in a dictionary, however it's still a dictionary (key-based) collection.
If you need to get the index of an object in a OrderedDictionary (or a hasthable) you need to use foreach-loop and a counter. Example (should be created as a function):
$hashTable = [Ordered]#{
'a' = 'blue';
'b'='green';
'c'='red'
}
$i = 0
foreach($key in $hashTable.Keys) {
if($key -eq "c") { $i; break }
else { $i++ }
}
That's how it works internaly too. You can verify this by reading the source code for OrderedDictionary's IndexOfKey method in .NET Reference Source
For the initial problem I was attempting to solve, a comparable process state, you can now use Enumerations starting with PowerShell v5.
You use the Enum keyword, set the Enumerators by name, and give them an integer value. The value can be anything, but I'm using ascending values starting with 0 in this example:
Enum _ProcessState{
Error = 0
None = 1
Started = 2
Paused = 3
Cleanup = 4
Complete = 5
Verified = 6
}
#the leading _ for the Enum is just cosmetic & not required
Once you've created the Enum, you can assign it to variables. The contents of the variable will return the text name of the Enum, and you can compare them as if they were integers.
$Item1_State = [_ProcessState]::Started
$Item2_State = [_ProcessState]::Cleanup
#return state of second variable
$Item2_state
#comparison
$Item1_State -gt $Item2_State
Will return:
Cleanup
False
If you wanted to compare and return the highest:
#sort the two objects, then return the first result (should return the item with the largest enum int)
$results = ($Item1_State,$Item2_State | Sort-Object -Descending)
$results[0]
Fun fact, you can also use arithmetic on them, for example:
$Item1_State + 1
$Item1_State + $Item2_State
Will return:
Paused
Verified
More info on Enum here:
https://blogs.technet.microsoft.com/heyscriptingguy/2015/08/26/new-powershell-5-feature-enumerations/
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_enum?view=powershell-6
https://psdevopsug.scot/post/working-with-enums-in-powershell/