Public/Get-DepartmentalGroupName.ps1

<#
.SYNOPSIS
    Builds a standardised departmental group name.

.DESCRIPTION
    The Get-DepartmentalGroupName function builds a group name from a prefix, a division, an optional
    department and an optional suffix, separated by hyphens. It only returns the name; it does not
    create anything in Microsoft Entra ID or Azure (use New-TcsEntraDepartmentalGroup for that).

    This command was called New-TcsDepartmentalGroup before version 0.2.0; that name still works as an alias.

    Naming rules:
      - '&' is read as 'and'. Apostrophes are removed ("Children's" -> 'Childrens'). Any other character
        that is not a letter, digit, space or hyphen (for example , ( ) / \ . :) is treated as a space.
      - Words are joined in PascalCase: the first letter of each word is upper-cased and the rest is kept
        as written, so acronyms stay as they are ('IT' -> 'IT', 'human resources' -> 'HumanResources').
        Culture-independent (invariant) casing is used, so the result is the same under any culture.
      - A hyphen inside a word ('E-Commerce') is kept. A hyphen on its own, or at the start or end of a
        word, separates parts of the name: 'Accounts Payable - UK' -> 'AccountsPayable-UK'. Stray
        hyphens and empty parts are dropped ('HR -' -> 'HR').
      - Without a department, the division is used in full ('SG-HumanResources').
      - With a department, the division is abbreviated to the initials of its words and the department
        is written in PascalCase ('SG-HR-Payroll'). All-caps words of 2 to 4 characters (acronyms) are
        kept whole in the initials ('IT Services' -> 'ITS'), and the words and, of, the and & are
        skipped ('Sales & Marketing' -> 'SM').
        Leading parts of the department that repeat the initials or the division ('HR - Payroll') are removed.
      - A part that repeats the part before it is not added twice ('HR' with department 'HR' -> 'SG-HR').
      - A division with no letters or digits left after clean-up (for example ',' or '&') is rejected
        with an error.

.PARAMETER Prefix
    The first part of the group name, for example 'SG' or 'DL'. Cannot contain whitespace.
    Accepts pipeline input by property name.

.PARAMETER Suffix
    An optional last part of the group name, for example 'Users'. Cannot contain whitespace.
    Accepts pipeline input by property name.

.PARAMETER Division
    The division the group belongs to, for example 'Human Resources'. Accepts pipeline input by
    property name, so a CSV with a Division column can be piped in.

.PARAMETER Department
    The optional department within the division, for example 'Payroll'. When supplied, the division is
    abbreviated to its initials. Accepts pipeline input by property name.

.INPUTS
    System.Management.Automation.PSObject
    Objects with Prefix, Suffix, Division and Department properties (for example rows from Import-Csv).

.OUTPUTS
    System.String

.EXAMPLE
    Get-DepartmentalGroupName -Prefix 'SG' -Division 'Human Resources'

    Returns 'SG-HumanResources'.

.EXAMPLE
    Get-DepartmentalGroupName -Prefix 'SG' -Division 'Human Resources' -Department 'HR - Payroll & Benefits' -Suffix 'Users'

    Returns 'SG-HR-PayrollAndBenefits-Users'.

.EXAMPLE
    Import-Csv .\departments.csv | Get-DepartmentalGroupName -Prefix 'SG'

    Returns one name for each row; the CSV has Division and Department columns.

.NOTES
    Author: Nigel Tatschner
    Company: TheCodeSaiyan
#>

function Get-DepartmentalGroupName {
    [CmdletBinding()]
    [Alias('New-TcsDepartmentalGroup')]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^\S+$')]
        [string]
        $Prefix,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidatePattern('^\S*$')]
        [string]
        $Suffix,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Division,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $Department
    )

    begin {
        $TelemetryArgs = @{
            ModuleName    = $MyInvocation.MyCommand.Module.Name
            ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
            CommandName   = $MyInvocation.MyCommand.Name
            ExecutionID   = [guid]::NewGuid().ToString()
        }
        Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer
        $lastError = $null
        $telemetrySent = $false
    }

    process {
        $completed = $false
        try {
            $groupName = ConvertTo-DepartmentalGroupName -Prefix $Prefix -Division $Division -Department $Department -Suffix $Suffix
            if (-not $groupName) {
                $exception = New-Object -TypeName System.ArgumentException -ArgumentList (
                    "The division '$Division' contains no letters or digits that can be used in a group name.", 'Division')
                $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception, 'InvalidDivision', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $Division
                $lastError = $errorRecord
                $PSCmdlet.WriteError($errorRecord)
                $completed = $true
                return
            }
            $groupName
            $completed = $true
        }
        catch {
            $lastError = $_
            throw
        }
        finally {
            # The end block does not run after a terminating error or when a downstream command
            # (for example Select-Object -First) stops the pipeline, so End telemetry is sent here.
            if (-not $completed -and -not $telemetrySent) {
                $telemetrySent = $true
                if ($lastError) {
                    Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $lastError
                }
                else {
                    Invoke-TelemetryCollection @TelemetryArgs -Stage End
                }
            }
        }
    }

    end {
        if (-not $telemetrySent) {
            $telemetrySent = $true
            if ($lastError) {
                Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $lastError
            }
            else {
                Invoke-TelemetryCollection @TelemetryArgs -Stage End
            }
        }
    }
}