0

Given a list of strings such as: apple01, apple02, and apple04, banana02, cherry01, how would you come up with the first available serial number of each type -- that is, apple03 if I ask about apple, or banana01 if I ask about banana, and cherry02 if I ask about cherry?

I'm tasked with automating the creation of Azure VM's, and these strings are actually host names of existing VM's, as reported by the Azure Powershell command (Get-AzureRmResourceGroupDeployment -ResourceGroupName "$azureResGrpName2").DeploymentName (or anything effectively similar).

Update: Here's my working code:

$rgdNames = (Get-AzureRmResourceGroupDeployment -ResourceGroupName "$azureResGrpName").DeploymentName
$siblings = $rgdNames | Where-Object{$_ -match "^($hostname)(\d+)$" }
if ($siblings) {
    # Make a list of all numbers in the list of hostnames
    $serials = @()
    foreach ($sibling in $siblings) {
        # $sibling -split ($sibling -split '\d+$') < split all digits from end, then strip off everything at the front
        # Then convert it to a number and add that to $serials
        $hostnumber = [convert]::ToInt32([string]$($sibling -split ($sibling -split '\d+$'))[1], 10)
        $serials += $hostnumber
    }
    foreach ($i in 1..$siblingsMax){ # Iterate over all valid serial numbers
        if (!$serials.Contains($i)) { # Stop when we find a serial number that isn't in the list of existing hosts
                $serial = $i
                break
            }
    }
} else {
    $serial = 1
}
KlaymenDK
  • 714
  • 9
  • 31
  • is the format always {name}{number}? if so sort by name, and then increment the number... – Michael B Jan 29 '16 at 13:37
  • Yes, the number-part is always last. But I can't be certain how many digits they are, _and_ I want to fill in any _gaps_ there might be (say, 1-2-4 should yield 3). Sorting does not solve that. – KlaymenDK Jan 29 '16 at 13:39
  • but is it always apple1, apple02,apple003? is {name} constant? and always followed by a {number} i.e. if you split them alpha / numerically – Michael B Jan 29 '16 at 13:41
  • How to handle leading zeros? For example I have `apple1`, `apple02` and `apple004`. What should be returned for `apple`? – user4003407 Jan 29 '16 at 13:50
  • @MichaelB, yes, the {name} part is constant insofar as that's what I provide to "filter" the list. – KlaymenDK Jan 29 '16 at 14:04
  • @PetSerAl, yeah that's a _real_ good question. Maybe we should assume that all serials are double-digit, always. – KlaymenDK Jan 29 '16 at 14:05
  • `write apple01 apple02 apple04 banana02 cherry01|?{$_-match'^(.+?)(\d+)$'}|%{$matches}|group{$_[1]}|%{$g=$_.Group|%{[int]$_[2]};'{0}{1:00}'-f$_.Name,(1..($_.Count+1)|?{$_-notin$g}|select -First 1)}` – user4003407 Jan 29 '16 at 14:18
  • Wait, just `write`, without a noun? How do I use this in Powershell? This looks complicated (which is _not_ to say I'm not grateful!). – KlaymenDK Jan 29 '16 at 14:27

4 Answers4

1

The naive and straight-forward solution would be based on pre-generating a list of syntax valid names.

Query names that are already in use and store the results into a collection.

Iterate the used names collection and remove those from the pre-generated collection.

Sort the pre-generated collection.

Now you got a collection that contains unused names in sorted order. Pick any number of desired names for further usage.

vonPryz
  • 22,996
  • 7
  • 54
  • 65
1

This seems to do the trick!

## Create variable to store number in 
$spareNumber = $null 
## we are presuming that they have already been seperated into groups 
$apples = @("apples01","apples002","apples3","apples15") 
## create an empty array 
$num = @() 
## You know what a foreach is right? ;) 
foreach ($apple in $apples)
{
    ## the hard working part 
    ## [convert]:: toint32 converts to, you guessed it... (and adds it to $num)
    ## $apple -split ($apple -split '\d+$') < split all digits from end, then strip off everything at the front 
    $num += [convert]::ToInt32([string]$($apple -split ($apple -split '\d+$'))[1], 10)
}
## count from 1 to 10, and pass to foreach 
(1..10) | foreach {
    ##'when we find one that isn't in $num, store it in sparenumber and break out of this joint. 
    if (!$num.Contains($_)) {
        $spareNumber = $_ 
        break 
    }
 }
 ## and here is your number... 
 $spareNumber 
Michael B
  • 11,887
  • 6
  • 38
  • 74
  • So are you expecting the op to make a function for this and he will group the fruits together? You are assuming there will only be 10 indexes possible? – Matt Jan 29 '16 at 16:09
  • 1
    @Matt In the comments the OP suggests that he already filters the list on 'fruits' - this is a bit of sample code to suggest how to find an available number. I am expecting that someone who is here, reading / understanding this question is capable of changing 1..10 to 1..100000 if needs be. – Michael B Jan 29 '16 at 16:13
  • I've used your code, BUT had to change `(1..10) | foreach {` to `foreach($i in 1..10){` in order to get `break` to not [break too far](http://stackoverflow.com/questions/35172403/why-does-my-break-statement-break-too-far). – KlaymenDK Feb 03 '16 at 09:34
  • @KlaymenDK I've updated the code to put that in, I'm surprised that's the only issue with it! it was more a mental exercise to see how to do it than actual considered code. But I'm glad it helped – Michael B Feb 03 '16 at 10:38
  • FYI, I didn't use all of it, mainly the loop design and your extract-and-convert command (see my update to the question, now with working code). – KlaymenDK Feb 03 '16 at 11:43
1

This could be the start of what you are looking for. I took a very similar approach as PetSerAl has done in his comment. I have made mine more verbose as it helps to take in what is happening here. Most of the explanation comes from comments in the code.

# Array of fruits/index values. Using string array as proof on concept.
$fruits = "apple01","apple02","apple04","banana02","cherry01"

# Regex -match the fruit name and index number. This also filters out lines that do not match the standard.
$fruityArray = $fruits | Where-Object{$_ -match '^(.+?)(\d+)$' } | ForEach-Object{
    # Create a simple object that splits up the matched info into a fruit index object
    [pscustomobject][ordered]@{
        Fruit = $matches[1]
        Index = [int]$matches[2]
    }
}

# Group by fruit and then we can find the next available index within the groups
$fruityArray | Group-Object Fruit | ForEach-Object{
    # For this fruit determine the next available index
    $thisFruitGroup = $_

    # Determine the highest index value. We add one in case all indexes are present from 1 to highest
    $highestPossibleIndex = ($thisFruitGroup.Group.Index | Measure-Object -Maximum).Maximum + 1

    # Check all possible indexes. Locating all that are free but filter the first one out
    $nextAvailableIndex = 1..$highestPossibleIndex | Where-Object{$_ -notin $thisFruitGroup.Group.Index} | Select -First 1

    # Take the fruit and add a padded index then pass that value to the pipeline. 
    '{0}{1:00}'-f $thisFruitGroup.Name, $nextAvailableIndex
}

We take the array of fruits and create an object array of fruit and indexes. Group those together by fruit and then determine the next available index based on all available indexes for that fruit. We add one to the highest possible index on the chance that they are all in use (no gaps). This is the case for cherry.

apple03
banana01
cherry02

Alternatively you could output the results to a variable and call the fruit you need from there if you don't need the whole list as output.

# Group by fruit and then we can find the next available index within the groups
$availableIndexes = $fruityArray | Group-Object Fruit | ForEach-Object{
    # For this fruit determine the next available index
    $thisFruitGroup = $_

    # Determine the highest index value. We add one in case all indexes are present from 1 to highest
    $highestPossibleIndex = ($thisFruitGroup.Group.Index | Measure-Object -Maximum).Maximum + 1

    # Check all possible indexes. Locating all that are free but filter the first one out
    $nextAvailableIndex = 1..$highestPossibleIndex | Where-Object{$_ -notin $thisFruitGroup.Group.Index} | Select -First 1

    # Take the fruit and add a padded index then pass that value to the pipeline. 
    [pscustomobject][ordered]@{
        Fruit = $thisFruitGroup.Name
        Index = $nextAvailableIndex
        String = '{0}{1:00}'-f $thisFruitGroup.Name, $nextAvailableIndex
    }
}

$availableIndexes | Where-Object{$_.Fruit -eq "Apple"}  | Select-Object -ExpandProperty String

Which would net the output of:

apple03
Community
  • 1
  • 1
Matt
  • 45,022
  • 8
  • 78
  • 119
  • Your second block seems to fit my bill very nicely (although I do kind of regret the 'fruit' naming in my example...). – KlaymenDK Jan 30 '16 at 22:10
  • @KlaymenDK I know fruit was not supposed to be what you are doing but I have to name the variables appropriately. – Matt Jan 30 '16 at 22:32
-2

if the name that occurs first is pure letters not including any number and than followed by a number . that would be easy and you can get the index of the first number or char that exists in that char list {1,2,3,4,5,6,7,8,9} and than you can make substring(0,indexof(firstnumber))