全角と半角を判別したい

タグの編集
投稿者 ヤマダ  (学生) 投稿日時 2023/1/26 15:16:39
全角と半角及び送信不可文字を判別したいのですが、最終的には以下のプログラムと同じ挙動になるようにしたいです。

chg_kana_name = student_name
Do Until chg_kana_name = ""

 If AscB(Left(chg_kana_name, 1)) >= &HA0 Or AscB(Left(chg_kana_name, 1)) <= &H1F Then
                        w_fullname = w_fullname & "?"
                        chg_kana_name = Mid(chg_kana_name, 2, Len(chg_kana_name))
 Else
  Exit Do
  End If
Loop

まず、上のIF文でどういう理屈で半角と全角を判定しているのか教えていただきたいです。
下記が自分なりに考えたソースになります。

chg_kana_name = student_name
Dim test As Byte()

Do Until chg_kana_name = ""
 test = System.Text.Encoding.GetEncoding("Unicode").GetBytes(Left(chg_kana_name, 1)
 If test(0) >= &HA0 Or test(0) <= &H1F Then
                            w_fullname = w_fullname & "?"
                            chg_kana_name = Mid(chg_kana_name, 2, Len(chg_kana_name))

 Else
  Exit Do
  End If
Loop

半角の場合はtest()のindex1の値は必ず255になるのでしょうか?
質問したいことが複数ありましたので、わかりにくい投稿かと思いますが、よろしくお願いいたします。
投稿者 (削除されました)  () 投稿日時 2023/1/26 20:33:30
(削除されました)
投稿者 るきお  (社会人) 投稿日時 2023/1/26 20:36:18
ごめんなさい。投稿いただいたプログラムから、どのような処理なのか読み取れませんでした。

投稿いただいたプログラムを見ると、たとえば、student_nameが"あいう"の場合、w_fullnameは""になり、student_nameが"アイウ"の場合は"???"になるように見えます。これは想定された動作ででしょうか?

というわけで全体的にはわからないのですが、半角判断についてはこの部分のようですね。
If AscB(Left(chg_kana_name, 1)) >= &HA0 Or AscB(Left(chg_kana_name, 1)) <= &H1F Then


これは、あまり良いプログラムではありませんが、意図は想像できます。
chg_kana_nameの1文字目をASCIIコードに置き換えた場合、A0 以上または 1F以下であれば半角と判断する意図のように見えます。
ASCIIコード(を日本語仕様で使う場合)では半角のアがB1,イがB2…ンがDDというように文字コードが割り当てられています。句読点や濁点などもあるので、A1~DFが一般的にいう半角カタカナです。
http://www.isc.meiji.ac.jp/~re00108/ch11/asciicode.html

A0以上または1F以下という範囲だと、これより範囲が広いので一般的には不適切な範囲だと思いますが、変数student_nameが改行以外のキーボードから入力できるASCII文字で構成されると前提であればいわゆる半角カタカナの範囲に合致します。
(たとえば、student_nameに漢字が入っていることがあるのであれば前提と異なります。)

ご質問では単に「半角」と表現されていますが、アルファベットの ABC などはここでは条件に合致しないようなので、問題となるのはいわゆる「半角カタカナ」のようですね。

「半角」や「全角」の定義は使う人によって異なっている場合があり、現在(2023年)安全なところではShift_JISで表現した場合に1バイトになる文字が半角文字、それ以外は全角文字というところだと思います。

半角文字は数が多くないので文字を決め打ちして判断してしまう方法もあります。

ヤマダさんが考えられたプログラムの方では、"Unicode"を使っているので、いわゆる文字コードは UTF-16 になります。これは ASCII とも Shift_JIS とも異なる文字コードです。(正確にはUTF-16はエンコーディング方式の名前でこれを「文字コード」というのは不正確です。)
だから、A0 以上 または 1F 以下という範囲ではいわゆる半角カタカナは判定できません。

UTF-16でいわゆる半角カタカナの文字コードは下記資料に記載されています。
https://www.unicode.org/charts/PDF/UFF00.pdf

アは FF71 、イは FF72, ンが FF9D です。
バイト型の配列 Byte() で表現すると、それぞれ FF, 71 と FF, 72 と FF, 9D です。
ですから、 test(0) のように先頭の1バイトだけを取り出すとどの文字も FF です。
FF は 10進数では 255 になります。

> 半角の場合はtest()のindex1の値は必ず255になるのでしょうか?
以上の通りなので、いわゆる半角カタカナをUTF-16で表現すると最初の1バイト目は必ず 255 になります。
Shift_JIS や 他の文字コード・エンコーディング方式の場合はそうとは限りません。


投稿者 (削除されました)  () 投稿日時 2023/1/26 21:28:13
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2023/1/26 21:41:23
> 最終的には以下のプログラムと同じ挙動になるようにしたいです。
そのプログラムが、どういう挙動を意図したものなのか、読み取れないです…。

目的によっては、正規表現を使って切り出せるかもしれません。
https://dobon.net/vb/dotnet/string/ishiragana.html

あるいは Shift_JIS 限定の文字列を前提としているような場合は、
Dim enc = System.Text.Encoding.GetEncoding(932, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback)
Dim binary As Byte() = enc.GetBytes(yourString)
などとして、元の文字列をまとめてバイナリ化できますので、そこから判断できるかもしれません。
(ExceptionFallback は、Shift_JIS で使えない文字をエラー扱いにするためのものです)


> 全角と半角及び送信不可文字を判別したいのですが
目的を教えてください。それらは『何のために』判別しようとしているのでしょうか?

というのも、全角/半角の定義というのは前提条件によって異なるためです。
送信不可文字の定義も然りです。さらに言えば
文字集合(charset)と符号化方式(encoding)の組み合わせにもよります。

たとえば iso-2022-jp エンコードな 7bit の text/plain メールにおいては、
"セ" という文字は機種依存文字として送信できないことになっていますが、
utf-8 エンコードな 8bit の text/plain メールであれば送出できます。
また、iso-2022-jp の 7bit メールであっても、text/html なメールであれば、
実体参照符号という仕組みを使うことで問題なく扱えます。

.NET の世界の文字集合は Unicode ですが、これも「ゼロ幅文字」や「結合文字」の
存在があるため、どうしても曖昧さが生じます。あるいは昔ながらの Shift_JIS であっても、
タブ文字や LF 文字を、半角とみなすか全角とみなすかは処理系依存です。

可読文字に限定した場合で考えてみても、たとえば罫線文字の "┣" は、
Windows 2000 の "メモ帳" の既定のフォント "MS ゴシック" では全角幅で描画されますが、
Windows 11 の "Windows コンソール" の既定のフォント "Cascadia Mono" では半角幅です。
いずれのフォントも、等幅の TrueType フォントであり、半角/全角いずれも間違いとは言えません。

Unicode の世界においては、EAST_ASIAN_WIDTH という文字幅情報が定義されているため、
これが判断条件に使われる処理系も多いです。これは、日中韓といった
東アジアの DBCS 混合幅文字エンコーディングへのマッピングの互換性のための情報で、
各文字が下記 6 種のいずれかに分類されています。

 F (Fullwidth)      全角。"¦" や "!" や "8" や "ヴ" や "a" や "$" や "₩" や "¢" など。
 H (Halfwidth)      半角。"│" や "ウ" や "₩" や "ᆭ" など。
 W (Wide)           ワイド。"❕" や "八" や "ウ" や "㏾" や "㌆" や "쪄" や "🤣" など。
 Na(Narrow)         ナロー。"|" や "!" や "$" や "8" や "{" や "¢" など。
 A (Ambiguous)      文脈依存。"│" や "∜" や "⑧" や "α" や "∀" や "🄉" など。
 N (Not East Asian) ニュートラル文字。"‼" や "Å" や "➑" や "ힰ" や "🤻" や "⁸" や "₈" など。



https://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt
https://www.unicode.org/reports/tr11/tr11-40.html
投稿者 ヤマダ  (学生) 投稿日時 2023/1/27 13:39:52
るきおさん

> 投稿いただいたプログラムを見ると、たとえば、student_nameが"あいう"の場合、w_fullnameは""になり、student_nameが"アイウ"の場合は"???"になるように見えます。これは想定された動作ででしょうか?

こちらは私が考えたプログラムの動作でしょうか?
私が考えたプログラムだと想定された動作ではありません。


> これは、あまり良いプログラムではありませんが、意図は想像できます。
chg_kana_nameの1文字目をASCIIコードに置き換えた場合、A0 以上または 1F以下であれば半角と判断する意図のように見えます。

文字コードによってバイト数の取り方が違うというの理解ができました。
下記のIF文の処理が読み取れずにいましたが、ASCIIコードとして半角とかどうか判定していたのですね、助かりました。
If AscB(Left(chg_kana_name, 1)) >= &HA0 Or AscB(Left(chg_kana_name, 1)) <= &H1F Then

頂いた資料を見てなんとかできそうです。
ありがとうございました。
投稿者 ヤマダ  (学生) 投稿日時 2023/1/27 14:41:25
すみません。
続けてここに質問してよいのかわかりませんが、させていただきます。
以下のようにchg_kana_name の値を左から順に半角判定をして半角の場合はfnc_chg_Kanaという関数でアルファベットに変換するという処理のつもりでプログラムしたのですが、test の値がずっと63で値が変化しません。文字が変われば値が変化していくと思ったのですが、どこが問題の箇所を教えていただきたいです。
よろしくお願いいたします。

chg_kana_name = "テスト セイト"
Do Until chg_kana_name = ""

 Dim test As Byte()
 Erase test
 test = System.Text.Encoding.GetEncoding("ASCII").GetBytes(Left(chg_kana_name, 1))

 If test(0) >= &HA0 Or test(0) <= &H1F Then
  w_fullname = w_fullname & "?"
  chg_kana_name = Mid(chg_kana_name, 2, Len(chg_kana_name))
  Continue Do
 End If

 w_fullname = w_fullname & fnc_chg_Kana(Left(chg_kana_name, 1))
  '変換した文字列分、元の文字列を削る
 chg_kana_name = Mid(chg_kana_name, 2, Len(chg_kana_name))

Loop
投稿者 (削除されました)  () 投稿日時 2023/1/27 16:27:55
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2023/1/27 17:04:13
> 最終的には以下のプログラムと同じ挙動になるようにしたいです。
このコードの出自はどちらでしょうか?

真偽判定で Or や And を使っていたり、Left や AscB を使っているので、
VB.NET ではなく、VBA や VBScript のコードだとは予想できますが、
参考にされている元ソースの実装が不自然で、コードの意図が読み解けない…。

(可能性1) 元ソースは「AscB(Left(」 ではなく、
 「AscW(Left(」や「Asc(Left(」や「AscB(LeftB(」だった。

(可能性2) 最初の質問にある student_name という変数の中身は、実際には
  student_name = StrConv("テスト セイト", vbFromUnicode)
 に相当する「バイナリ」であった。

(可能性3) 元ソースは、16bit バージョンの Visual Basic だった。

(可能性4) 元ソースでも、受け渡す文字列によっては適切に処理できずに化けることがある。
 (もしくは、化けるような種類の文字列が来ることが無い、という前提条件があった)


> 下記のIF文の処理が読み取れずにいましたが、ASCIIコードとして半角とかどうか判定していたのですね、助かりました。
>> If AscB(Left(chg_kana_name, 1)) >= &HA0 Or AscB(Left(chg_kana_name, 1)) <= &H1F Then
いえ。VBA の AscB 関数は ASCIIコードの値を得るためのものではありません。
渡された文字のバイナリ表現(≠ASCII)の、先頭データを整数で返すだけです。


たとえば、"あ" という文字は
 ASCII では <定義されていない文字>
 Shift_JIS では 82,A0
 UTF-16 では 42,30
 UTF-8 では E3,81,82
なのですが、たとえば、『MsgBox Hex(AscB("あ"))』という処理を実行した場合、
VB2 や VB4(16bit) では "82" 、VB4(32bit)~VB6 や VBA においては "42" と表示されます。

16bit 版 VB(日本語版) では Shift_JIS の先頭バイトの &H82 が返され、
32bit 版 VB では UTF-16 の先頭バイトの &H42 を読み取ることになります。
VBA (32bit/64bit) も UTF-16 相当なので &H42 です。

AscB(Left("テスト", 1)) の場合、先頭の "テ" が検査されて、VBA では &H83 が返されるでしょう。
なお、"テ" の文字を符号化した結果は下記の通り。
 ASCII では <定義されていない文字>
 Shift_JIS では C3
 UTF-16 では 83,FF
 UTF-8 では EF,BE,83


> Dim test As Byte()
> Erase test
> test = System.Text.Encoding.GetEncoding("ASCII").GetBytes(Left(chg_kana_name, 1))
Erase は明らかに冗長であり、下記の一行にまとめられますね。
Dim test = System.Text.Encoding.GetEncoding("ASCII").GetBytes(Left(chg_kana_name, 1))

文字を 1 文字ずつ変換するかわりに、すべてを一括変換することもできます。
Dim test = System.Text.Encoding.ASCII.GetBytes(chg_kana_name)


ところで、"テスト セイト" という 7 文字は、いずれも「ASCII という文字集合」では
定義されていない文字です。そのため、全ての文字が化けてしまい、下記の結果しか得られません。
Dim test As Byte() = { &H3F, &H3F, &H3F, &H3F, &H3F, &H3F, &H3F } '7バイト 


もしもこれを 「JIS コード」として処理するのであれば、こう書きます。
Dim test = System.Text.Encoding.GetEncoding(50222).GetBytes(chg_kana_name)

この場合は下記の結果が得られます。
Dim test As Byte() = { &H0E, &H43, &H3D, &H44, &H0F, &H1B, &H24, &H42, _
                       &H21, &H21, &H0E, &H3E, &H32, &H44, &H0F, &H1B, _
                       &H28, &H42 } '18バイト 


「シフトイン/シフトアウト無しの JIS コード」なら、こうです。
Dim test = System.Text.Encoding.GetEncoding(50221).GetBytes(chg_kana_name)

この場合は下記の結果が得られます。
Dim test As Byte() = { &H1B, &H28, &H49, &H43, &H3D, &H44, &H1B, &H24, _
                       &H42, &H21, &H21, &H1B, &H28, &H49, &H3E, &H32, _
                       &H44, &H1B, &H28, &H42 } '20バイト 


もしも VBA などの動作に揃えるのであれば、「既定のエンコーディング」を使うことができます。
Dim test = System.Text.Encoding.Default.GetBytes(chg_kana_name)

この場合は、日本語環境であれば下記の結果が得られます。
Dim test As Byte() = { &HC3, &HBD, &HC4, &H81, &H40, &HBE, &HB2, &HC4 } '8バイト 



やりたいことの仔細がまだ見えていないのですが、目的に近いのは Default エンコーディングかも…?
For Each c As Char In student_name
    Dim bin As Byte() = System.Text.Encoding.Default.GetBytes(c)

Next


> If test(0) >= &HA0 Or test(0) <= &H1F Then
VB.NET では And/Or ではなく、AndAlso/OrElse を使いましょう。

> w_fullname = w_fullname & "?"
ループ内で文字列連結する場合は、String ではなく StringBuilder を使いましょう。
投稿者 ヤマダ  (学生) 投稿日時 2023/1/27 17:56:53
魔界の仮面弁士さん

> このコードの出自はどちらでしょうか?
 恐らくVB6だと思います。曖昧で申し訳ございません。

> やりたいことの仔細がまだ見えていないのですが、目的に近いのは Default エンコーディングかも…?
元ソースがどの文字コードを使っているかは不明だったので、Default エンコーディングで試してみようと思います。
Default エンコーディングなんてあったのですね・・・。

やりたいことはstudent_name に入る文字列を1文字づつ半角全角を判定して、全角なら「?」に変換という処理を想定していました。
例えば「タナカ タロウ」なら「TANA? TA?U」という感じです。

元ソースの環境は恐らくVB6で私の環境がVB.NETなのでAscB関数が使えなく、同じ挙動になるにはどうすればいいかわからなかったので質問させていただきました。
最初に書くべきでした、申し訳ございません。

> VB.NET では And/Or ではなく、AndAlso/OrElse を使いましょう。
> ループ内で文字列連結する場合は、String ではなく StringBuilder を使いましょう。
どちらも処理の早くするためなのですね、勉強になりました。
投稿者 るきお  (社会人) 投稿日時 2023/1/27 19:40:25
>恐らくVB6だと思います。曖昧で申し訳ございません。
ヤマダさんは何でプログラムしたいのですか?

> student_name に入る文字列を1文字づつ半角全角を判定して、全角なら「?」に変換という処理を想定していました。
とりあえずExcel VBA でサンプルを作ってみました。
変数名は StudetName にしてしまいました。
StudentName に𩸽とかが入っているとおかしな結果になると思うので、実用に使用する場合は、StudentNameに入力される可能性のある文字に照らし合わせて、目的に適うか確認してくださいね。
半角全角判定にStrConvを使っているので、StrConvが使える環境に依存します。(たとえば、非日本語環境のWindowsでは動作しないと思います。)

Dim StudentName As String
    
StudentName = "タナカ タロウ"
  
Dim SafeStudentName As String
Dim i As Integer
Dim ZenkakuChar As String
Dim StudentChar As String
    
For i = 1 To Len(StudentName)
    'i文字目のStudentName 
    StudentChar = Mid(StudentName, i, 1)
        
    'それを全角に変換したもの 
    ZenkakuChar = StrConv(StudentChar, vbWide)

    '元の文字が全角に変換したものと同じなら、元の文字は全角(ということにする) 
    If ZenkakuChar = StudentChar Then
        StudentChar = "?"
    End If

    SafeStudentName = SafeStudentName & StudentChar

Next i

MsgBox SafeStudentName


>やりたいことはstudent_name に入る文字列を1文字づつ半角全角を判定して、全角なら「?」に変換という処理を想定していました。
>例えば「タナカ タロウ」なら「TANA? TA?U」という感じです。
「やりたいこと」と「例えば」があっていないですよね。全角なら?ということですが、半角がローマ字になっていますね。あと名前の区切りのスペースが全角スペースなので、「やりたいこと」通りに考えると「タナ??タ?ウ」になります。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2023/1/27 19:47:20
※文章を書いているうちにるきおさんと被ってしまいましたが、構わず投稿しちゃいます。(^^;

> やりたいことはstudent_name に入る文字列を1文字づつ半角全角を判定して、全角なら「?」に変換という処理を想定していました。
> 元ソースの環境は恐らくVB6で私の環境がVB.NETなのでAscB関数が使えなく、同じ挙動になるにはどうすればいいかわからなかったので質問させていただきました。
> 最初に書くべきでした、申し訳ございません。
そうですね。それを最初に書いていただけると、状況を掴みやすかったです。

で、その元ソースはどうやって入手したものですか?
VB6 だとしても、AscB と Left の組み合わせというのはちょっと不自然なので、
そもそも、移植元のソース自体にも何らかの問題を抱えている可能性も否めない…。


> 元ソースがどの文字コードを使っているかは不明だったので、Default エンコーディングで試してみようと思います。
> Default エンコーディングなんてあったのですね・・・。
.Default もしくは .GetEncoding(0) は、「OS の既定のコードページ」を意味します。

日本語環境であれば、コードページ932 (Shift_JIS 相当)になり、
簡体字中国語ならば、コードページ936 (GB2312 相当)になり、
繁体字中国語ならば、コードページ950 (Big5 相当)になります。



> 例えば「タナカ タロウ」なら「TANA? TA?U」という感じです。
 「タナカ タロウ」ならば「TANA? TA?U」だと思いますが
「タナカ タロウ」なので「TANA??TA?U」ではありませんか?


また、そうしたアルファベット変換を行う前に、プログラム側で
全角/半角を揃えておくといった事前作業は行われないのでしょうか。

Dim student_name = "タナカ タロウ"
'"タナカ タロウ" 
Dim han_student_name = StrConv(student_name, VbStrConv.Narrow Or VbStrConv.Katakana, &H411)
'"タナカ タロウ" 
Dim zen_student_name = StrConv(student_name, VbStrConv.Wide Or VbStrConv.Katakana, &H411)



そもそも、「アルファベットに変換」するための fnc_chg_Kana の実装内容も気になります。
変換というのがローマ字相当だとしたら、ちょっと無理がありませんか? 提示頂いたコードだと、
 fnc_chg_Kana(Left(chg_kana_name, 1))
のように 1 文字ずつ渡しているように見えますが、
半角カナの場合「゙」や「゚」が渡されることもあるわけですよね。
"ハハ"   → "HAHA"
"ババ" → "BABA"
"パパ" → "PAPA"



いわゆる『ローマ字』だとすれば、その表記は以下 3 種(あるいはその派生形)に分類されますが、
いずれも一文字単位では変換できないはずです。
 (1) ヘボン式 … ジ=JI、ヂ=JI、ヂャ=JA 、チ=CHI、ヲ=O
 (2) 日本式  … ジ=ZI、ヂ=DI、ヂャ=DYA、チ=TI 、ヲ=WO
 (3) 訓令式  … ジ=ZI、ヂ=ZI、ヂャ=ZYA、チ=TI 、ヲ=O

※学校教育で教えるのは 訓令式(とヘボン式)

撥音「ン」の変換ルールの問題もありますよね。
駅名表記(ヘボン式)の場合は、
 新宿 → shi N juku  ※唇を開いて発音する「ン」は N
 新橋 → shi M bashi ※唇を閉じて発音する「ン」は M
のように、次に続く文字が M, B, P の時は N ではなく M になるルールであり、
これを実装するには、1 文字ずつではなく、文字列全体を渡す必要があるはずです。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2023/1/27 20:22:59
渡される文字が、「Shift_JIS でいうところの 1 バイト文字」なのか
それとも「Shift_JIS でいうところの 2 バイト文字なのか」を
調べる関数を VB.NET で書いてみました。


''' <summary> 
''' Shift_JIS 変換時に、シングルバイト文字なら 1、ダブルバイト文字なら 2 を返す。どちらでもない場合は 0。 
''' </summary> 
Public Function GetCharWidth(c As CharAs Integer
    Static sjis As System.Text.Encoding = System.Text.Encoding.GetEncoding(932, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback)
    Try
        Return sjis.GetByteCount(c.ToString())
    Catch
        Return 0
    End Try
End Function