🐥

神話のプログラム言語 Odin 文字列操作編

2024/11/30に公開

Odin言語での、文字列操作について記載していきます。

文字列の扱い

Odinではstring型は、オブジェクトではありません。
以前にも記載しましたが、Odinのstring型は、rawptr+lenです。

str01/main.odin
package main

import "core:fmt"

main :: proc() {
  str := "abc ABC 1234"
  str2 := str   // pointerコピー (実体は同じ)
  fmt.printfln("str  addr=%v value=%v", raw_data(str), str)
  fmt.printfln("str2 addr=%v value=%v", raw_data(str2), str2)
}
$ odin run .\str01\
str  addr=0x7FF7B5062DA8 value=abc ABC 1234
str2 addr=0x7FF7B5062DA8 value=abc ABC 1234  ← アドレスが同じ位置を示す

上記のように、string型に代入式を渡すだけでは、実際のデータはコピーされません。
これは、あくまでポインタをコピーしただけです。
実データをコピーする場合は、下記のように行います。

str02/main.odin
package main

import "core:fmt"
import "core:strings"

main :: proc() {
  str := "abc ABC 1234"
  str2 := strings.clone(str)   // byteをallocして、そのアドレスを返す
  fmt.printfln("str  addr=%v value=%v", raw_data(str), str)
  fmt.printfln("str2 addr=%v value=%v", raw_data(str2), str2)
}
$ odin run .\str02\
str  addr=0x7FF681B8AFA0 value=abc ABC 1234
str2 addr=0x1CA2526C7E8 value=abc ABC 1234  ← 異なるアドレス位置を示す

string.clone関数は、渡されたデータの長さを元に、byte配列をアロケートし、データを設定してから、そのアドレスを返します。

また、bprintaprint関数と言うのがOdin言語にはあり、これは、C言語で言うsprintf関数と同じです。特にbprint関数は、byte配列を用意し、それを引数に渡して、加工されたstring型を返します。

package main

import "core:fmt"

main :: proc() {
  bytes: [100]byte

  // bytesバッファにデータを格納し、文字列を返す
  s_str := fmt.bprintf(bytes[:], "bprint関数は、バイト列に値を設定し、string型で返す : %v", 12345)
  fmt.printfln("bprint関数の値=[%v]", s_str)

  // aprint関数は、bprint関数と同じだが、引数指定で、allocatorを渡せる、デフォルトはtemp_allocatorに格納
  a_str := fmt.aprintf("aprint関数は、String値を返す : %v", 67890)
  fmt.printfln("aprint関数の値=[%v]", a_str)
}
$ odin run .\str03\
bprint関数の値=[bprint関数は、バイト列に値を設定し、string型で返す : 12345]
aprint関数の値=[aprint関数は、String値を返す : 67890]

文字列操作

Odinでの文字操作は、基本、import "core:strings"ライブラリ内の関数で行う事が可能です。下記に、stringsライブラリ内の関数を使った操作方法を記載します。

  // 前後のトリムだけ
  str3 := strings.trim("  a  b   c   ", " ")
  fmt.printfln("[%v]", str3)    // [a  b   c]

  // 存在チェック
  fmt.println( strings.contains_any("test", "ts") ) // true

  // 指定文字列の位置表示
  fmt.println(strings.index("test", "st"))  // 2

  // 指定文字列の数を表示
  fmt.println(strings.count("abbccc", "b")) // 2

  // 前後のバイト数を削除
  fmt.println(strings.cut("some example text", 5, 7))  // "example"
  for char in strings.fields(" abb ccc ") {  // [abb, ccc]
    fmt.printfln(char)
  }

  // 文字列の分割
  fmt.println(strings.split("aa,bbbb,ccc", ","))  // ["aa", "bbbb", "ccc"]

  // 何か違う気がする。バグってる?
  fmt.println(strings.scrub("Hello\xC0\x80World", "?")) // Hello?

  // Upper、Lower変換
  fmt.println(strings.to_upper("abcx")) // ABCX
  fmt.println(strings.to_lower("BACK")) // back

  // 文字の配列を繋げる
  a := [?]string { "a", "b", "c" }
  fmt.println(strings.concatenate( a[:]) )  // abc
  fmt.println(strings.concatenate( {"明日", "天気になあれ"} ))  // 明日天気になあれ

  // 文字列を繋げる
  b := [?]string { "a", "b", "c" }
  fmt.println(strings.join(b[:], "-"))  // a-b-c

日本語文字列操作

Odinで、日本語を扱う場合は、rune関数を使います。
ルーンって何?と思われますが、ルーン文字はゲルマン諸国の古代文字であり、Odin言語ではUTF8の事をルーン文字と呼びます。
つまり、日本語もルーン文字なのです。(ある意味カッコいい!

※上記画像が本来のルーン文字

  str := "おはようabc漢字"
  fmt.printfln("str = %v length = %v", str, len(str))    // str = おはようabc漢字 length = 21
  // スライスだとバイト数で計算するため、文字化けする。
  fmt.printfln("str[1:] = %v", str[1:])                  // str[1:] = ??はようabc漢字

  // runeは文字UTF8も1文字として考える
  str2, _ := strings.substring(str, 2, strings.rune_count(str))
  fmt.printfln("str = %v", str2)                         // str = ようabc漢字
  
  ok := strings.contains_rune(str, 'あ')                 // 存在チェック 存在すればtrue
  fmt.printfln("bool = %v", ok)                          // false

  pos := strings.index_rune(str, 'う')                   // 検索rune文字の位置(バイト数)を返す、存在しない場合は-1
  // バイト数が変える為、スライスで分割可能
  fmt.printfln("position = %v str = %v", pos, str[pos:]) // position = 9 str = うabc漢字

  rune_chars := utf8.string_to_runes(str)                 // rune文字を分割
  for char in rune_chars {
    fmt.printfln("char = %v", char)           // お, は, よ, う, a, b, c, 漢, 字
  }

最初のstr[1:]を出力した場合、文字化けをおこします。これは、スライス[:]がバイトで表現されているためです。つまり、UTF8は3バイトで1文字になるため、スライス[1:]では、1バイト目から出力されるため文字化けが発生します。
その次の、substring関数はルーン文字に対応しているため、UTF8文字の3文字目から正常に出力されます。

UTF8文字列をSJISに変換

Windows環境でUTF8をSJISに変換する場合は、以下の様に行います。

str03/main.odin
package main

import "core:fmt"
import "core:os"
import "core:sys/windows"
import "core:c"

// コード変換
CP_SHIFTJIS           :: 932   // SJIS
CP_JIS                :: 50220 // JIS
CP_EUC                :: 20932 // EUC
CP_UTF8               :: 65001 // UTF8
WC_DISCARDNS          :: 0x00000010 // 変換時にノンスペーシングキャラクタを読み捨てます。
WC_SEPCHARS           :: 0x00000020 // 変換時に separate characters に変換する。(デフォルト)
WC_DEFAULTCHAR        :: 0x00000040 // 変換時に例外をデフォルトキャラクタに置きかえます。
WC_COMPOSITECHECK     :: 0x00000200 // composite キャラクタを precomposed キャラクタに変換します。
WC_NO_BEST_FIT_CHARS  :: 0x00000400 // Windows 2000/XP: 直接マルチバイト文字に変換できない文字をデフォルトキャラクタに置きかえます。

main :: proc() {
  defer free_all(context.temp_allocator)

  str := "おはようabc漢字"
  if out, ok := utf8_to_sjis(str); ok {
    fmt.println("i=", len(out), str)
    os_write("sjis.txt", out)
  
    sjis := os_read("sjis.txt")
    if utf8, ok_ := sjis_to_utf8(sjis); ok {
      fmt.printfln("utf8=[%v]", utf8)
    }
  }
}

// utf8 -> sjis変換 (windows only)
utf8_to_sjis :: proc(utf8: string, allocator:= context.temp_allocator) -> (string, bool) {
  // utf8 -> unicode
  len_Unicode := windows.MultiByteToWideChar(CP_UTF8, windows.MB_ERR_INVALID_CHARS, raw_data(utf8), i32(len(utf8)), nil, 0)
  if len_Unicode == 0 {
    return "", false
  }
  buf_Unicode := make([]u16, len_Unicode+1, allocator)
  windows.MultiByteToWideChar(CP_UTF8, windows.MB_ERR_INVALID_CHARS, raw_data(utf8), i32(len(utf8)), raw_data(buf_Unicode), len_Unicode)

  // unicode -> sjis
  len_SJIS := windows.WideCharToMultiByte(CP_SHIFTJIS, WC_COMPOSITECHECK|WC_SEPCHARS, raw_data(buf_Unicode), -1, nil, 0, nil, nil)
  buf_SJIS := make([]byte, len_SJIS, allocator)
  len := windows.WideCharToMultiByte(CP_SHIFTJIS, WC_COMPOSITECHECK|WC_SEPCHARS, raw_data(buf_Unicode), len_Unicode+1, raw_data(buf_SJIS), len_SJIS, nil, nil)
  return string(buf_SJIS[:]), true
}

// sjis -> utf8変換 (windows only)
sjis_to_utf8 :: proc(sjis: string, allocator:= context.temp_allocator) -> (string, bool) {
  // sjis -> unicode
  len_Unicode := windows.MultiByteToWideChar(CP_SHIFTJIS, windows.MB_ERR_INVALID_CHARS, raw_data(sjis), i32(len(sjis) + 1), nil, 0)
  if len_Unicode == 0 {
    return "", false
  }
  buf_Unicode := make([]u16, len_Unicode, allocator)
  windows.MultiByteToWideChar(CP_SHIFTJIS, windows.MB_ERR_INVALID_CHARS, raw_data(sjis), i32(len(sjis) + 1), raw_data(buf_Unicode), i32(len(buf_Unicode)))

  // unicode -> utf8
  len_UTF8 := windows.WideCharToMultiByte(CP_UTF8, WC_COMPOSITECHECK|WC_SEPCHARS, raw_data(buf_Unicode), -1, nil, 0, nil, nil)
  buf_UTF8 := make([]byte, len_UTF8, allocator)
  windows.WideCharToMultiByte(CP_UTF8, WC_COMPOSITECHECK|WC_SEPCHARS, raw_data(buf_Unicode), len_Unicode + 1, raw_data(buf_UTF8), len_UTF8, nil, nil)

  return string(buf_UTF8[:]), true
}

// ファイルの書き込み関数
os_write :: proc(filename: string, data: string) {
  if fd, ok := os.open(filename, os.O_CREATE|os.O_RDWR); ok == nil {
    defer os.close(fd)
    b := transmute([]byte)data
    os.write(fd, b)
    os.flush(fd)
  }
}

// ファイルの読み込み関数
os_read :: proc(filename: string, allocator:= context.temp_allocator) -> string {
  if fd, ok := os.open(filename, os.O_RDONLY); ok == nil {
    defer os.close(fd)
    b := make([]byte, 1024, allocator)
    total, ok := os.read(fd, b)
    return string(b[:total])
  }
  return ""
}

上記プログラムは、UTF8文字をSJISに変換し、ファイルに退避した後、再度、SJISをUTF8に変換しています。MultiByteToWideChar関数でUTF8をunicode(UTF16BL)文字に変換し、更にWideCharToMultiByte関数でunicode(UTF16BL)をSJIS変換します。2度呼び出しているのは、文字数を求める為です。

Odin言語では、Windows上の文字変換は、Win32APIを使って行いますが、LinuxやMacでは逆にiconv関数で変換が行えます。LinuxやMacは、POSIX基準であるため、libiconvライブラリが標準でOSに入っているためですが、WindowsはVisualC++で実行するため、Win32APIを利用して、上記のように記載しなければ、いけません。

おわりに

文字変換について、色々なプログラム言語を調べてみましたが、NimやZigはPOSIX準拠のmingw64を使っているため、iconv関数での変換で統一されています。また、Rustはencoding_rsと言う独自ライブラリがありますが、これはGecko用のバイナリ変換コードを持っています。(ただ、Rustのencoding_rsライブラリはstatic libraryだと思うのですが、これを設定するだけで実行モジュールが500kバイトほど増える気がします。)

フォーラムで、WindowsOSだけMVCを使うと、統一性が無くなると、gingerBill氏に問いかけた方がいましたが、gingerBillの回答は、外部ライブラリがWin32APIに対応しているのが多いので、敢えてVisualC++を使っているとの事でした。

Discussion