Private/Get-VellumPdfNumberShape.ps1

# Whole text: optional sign, digits, single '.' or ',' between digit runs. Compiled once per session.
$script:VellumPdfNumberRegex = [regex]::new('^([+-]?)([0-9]+(?:[.,][0-9]+)*)$', [System.Text.RegularExpressions.RegexOptions]::Compiled)

function Get-VellumPdfNumberShape {
    <#
    .SYNOPSIS
        Classifies a string as a number, an ambiguous number, or not a number, from its separators alone.
    .DESCRIPTION
        The lexical half of ConvertFrom-VellumPdfNumberText (auto-detection, no culture). The whole trimmed
        text must be an optional sign followed by digits with single '.' or ',' separators between digit
        runs. The rules:

          1. Both separators present: the LAST one is the decimal mark, the other is the grouping mark
             (1.444,50 and 1,976.08). The decimal mark may appear only once.
          2. One separator appearing more than once: grouping (1.234.567).
          3. One separator appearing once: a decimal mark, unless exactly three digits follow it and at
             most three precede it; with more than three digits before it grouping is impossible, so it is a
             decimal mark too (12345,678).
          4. Grouping must be valid: the first group has 1 to 3 digits, every later group exactly 3.
          5. Leading zeros in a multi-digit integer part (007, 01234) are not numbers.
          6. One separator, once, exactly three digits after and 1 to 3 (not starting with 0) before
             (1,234 and 1.234) is Ambiguous: the caller decides from other evidence.

        Returns an object with Kind ('None', 'Number' or 'Ambiguous'). A Number carries Sign, Integer (digits,
        no grouping), Fraction (digits or ''), Decimal (the decimal mark the text used, or '') and Group (the
        grouping mark the text used, or ''). An Ambiguous carries Sign, Separator, Left and Right.
    #>

    [CmdletBinding()]
    param(
        [AllowNull()] [AllowEmptyString()] [string]$Text
    )

    $none = [pscustomobject]@{ Kind = 'None'; Sign = ''; Integer = ''; Fraction = ''; Decimal = ''; Group = ''; Separator = ''; Left = ''; Right = '' }
    if ([string]::IsNullOrEmpty($Text)) { return $none }
    $match = $script:VellumPdfNumberRegex.Match($Text.Trim())
    if (-not $match.Success) { return $none }
    $sign = $match.Groups[1].Value
    $body = $match.Groups[2].Value

    # Valid grouping: 1-3 digits, then groups of exactly 3, and no leading zero on a grouped integer.
    $validGroups = {
        param([string[]]$groups)
        if ($groups[0].Length -gt 3 -or $groups[0][0] -eq '0') { return $false }
        for ($i = 1; $i -lt $groups.Count; $i++) { if ($groups[$i].Length -ne 3) { return $false } }
        $true
    }
    $number = {
        param($integer, $fraction, $decimalMark, $groupMark)
        [pscustomobject]@{ Kind = 'Number'; Sign = $sign; Integer = $integer; Fraction = $fraction; Decimal = $decimalMark; Group = $groupMark; Separator = ''; Left = ''; Right = '' }
    }

    $dots = $body.Length - $body.Replace('.', '').Length
    $commas = $body.Length - $body.Replace(',', '').Length

    if ($dots -eq 0 -and $commas -eq 0) {
        if ($body.Length -gt 1 -and $body[0] -eq '0') { return $none }
        return (& $number $body '' '' '')
    }

    if ($dots -gt 0 -and $commas -gt 0) {
        $decimalMark = if ($body.LastIndexOf('.') -gt $body.LastIndexOf(',')) { '.' } else { ',' }
        $groupMark = if ($decimalMark -eq '.') { ',' } else { '.' }
        if ((($decimalMark -eq '.') ? $dots : $commas) -ne 1) { return $none }
        $parts = $body.Split($decimalMark)
        $groups = $parts[0].Split($groupMark)
        if (-not (& $validGroups $groups)) { return $none }
        return (& $number ($groups -join '') $parts[1] $decimalMark $groupMark)
    }

    $separator = if ($dots -gt 0) { '.' } else { ',' }
    $count = [Math]::Max($dots, $commas)
    $parts = $body.Split($separator)
    if ($count -gt 1) {
        if (-not (& $validGroups $parts)) { return $none }
        return (& $number ($parts -join '') '' '' $separator)
    }
    $left = $parts[0]
    $right = $parts[1]
    if ($left.Length -gt 1 -and $left[0] -eq '0') { return $none }
    if ($right.Length -eq 3 -and $left.Length -le 3 -and $left[0] -ne '0') {
        return [pscustomobject]@{ Kind = 'Ambiguous'; Sign = $sign; Integer = ''; Fraction = ''; Decimal = ''; Group = ''; Separator = $separator; Left = $left; Right = $right }
    }
    & $number $left $right $separator ''
}