全角と半角が混在する文字列の表示幅について

# コンソール上での表示幅(≠文字数)

コンソール上で表示される和文の表示幅（半角文字幅で数えた場合の文字数。以下、単に「幅」と記す）を得るには、その文字列に含まれる各々の文字について全角または半角のどちらであるかを判別する必要があります。ネット上で調べると、全角／半角の判別にはShift-JISエンコーディングでの文字のバイト数を利用する例が多いです。ここでは、VBのStrConv()関数で全角に変換した文字列と元の文字列を文字毎に比較して、一致する文字を全角、そうでない文字を半角と判定する方法でコンソール上での幅を求める例を紹介します。

```powershell:
# by earthdiver1

Add-Type -AssemblyName "Microsoft.VisualBasic"

Function Get-StringWidth([String]$String) {
    $wide  = [Microsoft.VisualBasic.Strings]::StrConv($String,[Microsoft.VisualBasic.VbStrConv]::Wide)
    $width = 0
    for ($i=0; $i -lt $String.Length; $i++) {
        $width++
        if ($String[$i] -eq $wide[$i]) { $width++ }
    }
    return $width
}
```

#指定した幅に文字列を加工する

下のコマンドレットは、与えられた文字列を、第1引数で指定した最大幅に合わせて成形します。文字列は-Stringオプションで指定するかパイプで渡します。-Ellipses オプションは入力文字列が切り詰められた場合に右端を"..."に変えます。-Padding オプションは入力文字列の幅が第1引数で指定された最大幅よりも小さい場合に空白でパディングをおこないます。

```powershell:
# by earthdiver1

Function FormatString {
    [CmdletBinding(PositionalBinding=$False,
                   DefaultParameterSetName="NoEllipses")]
    Param (
        [parameter(Position=0,Mandatory=$True,ParameterSetName="NoEllipses")]
        [ValidateRange(0, [Int]::MaxValue)]
        [Int] $MaxWidth_NoEllipses,

        [parameter(Position=0,Mandatory=$True,ParameterSetName="Ellipses")]
        [ValidateRange(3, [Int]::MaxValue)]
        [Int] $MaxWidth_Ellipses,

        [parameter(Position=1,Mandatory=$True,ValueFromPipeline=$True)]
        [AllowEmptyString()]
        [String] $String,

        [Parameter(ParameterSetName="Ellipses")]
        [Switch] $Ellipses,

        [ValidateRange(0, [Int]::MaxValue)]
        [Int] $AbbrevPosition = $MaxWidth_Ellipses - 3,

        [Switch] $Padding
    )
    BEGIN {
        switch ($PsCmdlet.ParameterSetName) { 
            "NoEllipses"  { $MaxWidth = $MaxWidth_NoEllipses }
            "Ellipses"    { $MaxWidth = $MaxWidth_Ellipses   }
        }
        Add-Type -AssemblyName "Microsoft.VisualBasic"
        $SB = New-Object System.Text.StringBuilder
    }
    PROCESS {
        $wide  = [Microsoft.VisualBasic.Strings]::StrConv($String,[Microsoft.VisualBasic.VbStrConv]::Wide)
        $width = 0
        for ($i=0; $i -lt $String.Length; $i++) {
            $width++
            if ($String[$i] -eq $wide[$i]) { $width++ }
        }
        if ($width -le $MaxWidth) {
            if ($Padding) {
                return $String + " " * ($MaxWidth - $width)
            } else {
                return $String
            }
        }
        $width = 0
        if ($Ellipses) {
            $AbbrevPosition = [Math]::Min($AbbrevPosition, $MaxWidth-3)
            for ($i=0; $i -lt $String.Length; $i++) {
                if ($String[$i] -eq $wide[$i]) { $w = 2 } else { $w = 1 }
                if ($width + $w -gt $AbbrevPosition) { break }
                [void]$SB.Append($String[$i])
                $width += $w
            }
            [void]$SB.Append("...")
            $width += 3
            if ($AbbrevPosition -lt $MaxWidth - 3) {
                $InsertPosition = $SB.Length
                for ($i=$String.Length-1; $i -ge 0; $i--) {
                    if ($String[$i] -eq $wide[$i]) { $w = 2 } else { $w = 1 }
                    if ($width + $w -gt $MaxWidth) { break }
                    [void]$SB.Insert($InsertPosition,$String[$i])
                    $width += $w
                }
            }
        } else {
            for ($i=0; $i -lt $String.Length; $i++) {
                if ($String[$i] -eq $wide[$i]) { $w = 2 } else { $w = 1 }
                if ($width + $w -gt $MaxWidth) { break }
                [void]$SB.Append($String[$i])
                $width += $w
            }
        }
        $newString = $SB.ToString()
        [void]$SB.Clear()
        if ($Padding -and $width -lt $MaxWidth) { 
            return $newString + " "
        } else {
            return $newString
        }
    }
}
```

##使用例

```
> FormatString 30 -s "Qiitaは、プログラマのための技術情報共有サービスです。"
Qiitaは、プログラマのための技

> FormatString 30 -s "Qiitaは、プログラマのための技術情報共有サービスです。" -e
Qiitaは、プログラマのための...

> '"' + (FormatString 30 -s "Qiitaは、プログラマのための技術情報共有サービスです。") + '"'
"Qiitaは、プログラマのための技"

> '"' + (FormatString 30 -s "Qiitaは、プログラマのための技術情報共有サービスです。" -p) + '"'
"Qiitaは、プログラマのための技 "

> '"' + (FormatString 30 -s "Qiitaは、プログラマのための技術情報共有サービスです。" -e) + '"'
"Qiitaは、プログラマのための..."

> '"' + (FormatString 30 -s "Qiitaは、プログラマのための技術情報共有サービスです。" -e -p) + '"'
"Qiitaは、プログラマのための..."

> '"' + (FormatString 31 -s "Qiitaは、プログラマのための技術情報共有サービスです。") + '"'
"Qiitaは、プログラマのための技術"

> '"' + (FormatString 31 -s "Qiitaは、プログラマのための技術情報共有サービスです。" -p) + '"'
"Qiitaは、プログラマのための技術"

> '"' + (FormatString 31 -s "Qiitaは、プログラマのための技術情報共有サービスです。" -e) + '"'
"Qiitaは、プログラマのための..."

> '"' + (FormatString 31 -s "Qiitaは、プログラマのための技術情報共有サービスです。" -e -p) + '"'
"Qiitaは、プログラマのための... "

> '"' + (FormatString 60 -s "Qiitaは、プログラマのための技術情報共有サービスです。") + '"'
"Qiitaは、プログラマのための技術情報共有サービスです。"

> '"' + (FormatString 60 -s "Qiitaは、プログラマのための技術情報共有サービスです。" -p) + '"'
"Qiitaは、プログラマのための技術情報共有サービスです。       "

```

# 注意
マルチバイト言語をサポートしていない環境下（英語OSなど）で使用すると System.ArgumentException のシステム例外が発生します。

#ちなみに
全角文字が含まれない場合（英数字のみの場合など）、上記の Get-StringWidth関数と FormatString コマンドレットは下記の様に簡潔に書けます。

```powershell:
Function Get-StringWidth([String]$String) {
    return $String.Length
}

Function FormatString {
    [CmdletBinding(PositionalBinding=$False,
                   DefaultParameterSetName="NoEllipses")]
    Param (
        [parameter(Position=0,Mandatory=$True,ParameterSetName="NoEllipses")]
        [ValidateRange(0, [Int]::MaxValue)]
        [Int] $MaxWidth_NoEllipses,

        [parameter(Position=0,Mandatory=$True,ParameterSetName="Ellipses")]
        [ValidateRange(3, [Int]::MaxValue)]
        [Int] $MaxWidth_Ellipses,

        [parameter(Position=1,Mandatory=$True,ValueFromPipeline=$True)]
        [AllowEmptyString()]
        [String] $String,

        [Parameter(ParameterSetName="Ellipses")]
        [Switch] $Ellipses,

        [ValidateRange(0, [Int]::MaxValue)]
        [Int] $AbbrevPosition = $MaxWidth_Ellipses - 3,

        [Switch] $Padding
    )
    BEGIN {
        switch ($PsCmdlet.ParameterSetName) { 
            "NoEllipses"  { $MaxWidth = $MaxWidth_NoEllipses }
            "Ellipses"    { $MaxWidth = $MaxWidth_Ellipses   }
        }
    }
    PROCESS {
        if ($Ellipses) {
            $AbbrevPosition = [Math]::Min($AbbrevPosition, $MaxWidth-3)
            $newString = $String -Replace "^(.{$AbbrevPosition}).{4,}(.{$($MaxWidth-$AbbrevPosition-3)})$",'$1...$2'
        } else {
            $newString = $String -Replace "^(.{$MaxWidth}).*$",'$1'
        }
        if ($Padding -and $newString.Length -lt $MaxWidth) {
            $newString = "{0,$(-$MaxWidth)}" -F $newString
        }
        return $newString
    }
}
```

#追記
2017/11/20 可読性をよくするため FormatString コマンドレットを書き直しました（ついでに3点リーダーの位置を指定できるようにしました）。

クリエイティブ・コモンズ 表示 - 継承 4.0 国際