Rust コード例
Rust コード例
このページは Rust のプログラム例を紹介するものです。
このページは、AI に教えてもらったコードを中心に紹介します。
AI (ChatGPT 、Gemini Pro 、Claude 、Qwen(千問) ) のプログラミング言語の知力は優れたものです。
AI は、開発したいプログラムの仕様を伝えると、要求した動作をする日本語コメント付きのコードを、瞬時に返答してくれます。
プログラミング言語の学習や、プログラム開発で、AI は無料で利用できる、便利な手段・ツールだと思います。
Rust のコードを書くときの注意:
1.プログラムのファイル(拡張子 .rs)の文字コードは、UTF-8 です。Shift-JIS でないことに注意下さい。
2.ファイルのフルパスに使う区切文字は、スラッシュではなくバックスラッシュ です。
Rust のパス区切りは バックスラッシュ:File::open("Z:/ SortTest/ utf_ken_all.csv")?;
Windows のパス区切りではダメ!! :File::open("Z:\ SortTest\ utf_ken_all.csv")?;
目次
Hello, World!
処理時間(経過時間)を表示
素因数分解
・1.素因数分解(試し割り法)
・2.素因数分解(ライブラリ使用)
・整数型の最小値、最大値を表示
文字コード UTF-16 でファイル保存
・1.標準ライブラリのみで行う(外部クレート不要
・2.`encoding_rs` を使う
・3.保存したファイルの文字コードを確認する
・4.UTF-16 のファイルを、Rust で読み込む
テキスト(ShiftJIS)を UTF-8 に
テキスト・ファイルを行ソート
・テキスト・ファイルを行ソート ChatGPT
・テキスト・ファイルを行ソート Gemini
・テキスト・ファイルを行ソート Claude
テキストに含まれる文字の文字毎の出現回数
機能目次
・エラーで画面が閉じたときのエラー内容を確認する
・Enter 入力で画面を閉じる
・Key 入力で画面を閉じる
・処理時間を表示
・整数型の最小値、最大値
・ファイルを開くダイアログ1
・ファイルを開くダイアログ2
・UTF-8(BOM付き)で保存
・読込んだテキストと同じ文字コードで保存
・テキスト・ファイル読込み(バイナリ)
・テキスト・ファイル読込み(文字コード判別)
・ファイルの文字コード判定
・HashMap(連想配列)
・並び替え(sort_by)
・並び替え(sort_unstable)
・繰返し.iter()
コード例
Hello, World!
プログラム言語を学ぶ際に、 Hello, world! というテキストを画面に出力する小さなプログラムを書くことは伝統的なことです。
The Rust Programming Language 日本語版にコードが紹介されています。
https://doc.rust-jp.rs/book-ja/ch01-02-hello-world.html
しかし、このコードをコンパイルして実行しても、画面が直ぐ閉じてしまって、肝心の文字が見えません(^^ゞ
実行画面で、何かキー入力するまで表示を止めておくように修正したコードを紹介します。
下のコードは、Enter するまで画面を表示しています。
ライブラリを使っていないので、このファイルだけで rustc helloJP.rs でコンパイルできます。
helloJP.rs
// helloJP.rs
//rustc helloJP.rs
use std ::io;
fn main()
{
// 世界よ、こんにちは
println! ("こんにちは, 世界!");
// 標準入力を待機
println! ("");
let mut 入力 = String::new();
println! ("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
}
解説:
use 文:幾つかのモジュールを使うという宣言
同じ名前の人が複数いるときは、苗字::名前で呼ばないと特定できません。このとき「::」名前空間を使います。
std::env::args() などの「::」 は、 モジュールの名前空間を区切るものです。
モジュールとは名前空間によって階層化された外部ライブラリで、 この場合は std 名前空間の env モジュールにある args() メソッドを意味します。
std::env::args() ならば、std 名前空間の env モジュールにある args() メソッドを意味します。
std::io::File は主にファイルハンドルを保持する構造体です。
std::io::Error はエラーの内容を保持する構造体です。
std::ops::AddAssign は加算代入演算子「+=」 に必要とされるメソッドを実装するトレイトです。
std::fs モジュール……ファイルのオープンや作成、 ファイル/ディレクトリの操作
std::io モジュール……入出力関連のトレイト、標準入出力など
println! :文字列をコンソール(実行画面)に表示するためのマクロです。
関数のように見えて最後に! が付くのがマクロです。関数と違うのは関数呼び出し(function call)を生成する代わりに、ソースコード中に展開され、周囲のプログラムとともにコンパイルされる点です。
下のコードは、何かキー入力するまで画面表示を継続します。
Windows, UNIX両方をサポートするクロスプラットフォームなターミナル操作ライブラリ crossterm を利用しているので、プロジェクト(Hello_World )を作ってコンパイルする必要があります。
crossterm を使うとテキストベースのインターフェースを作成できます。
Cargo.toml
[package]
name = "Hello_World"
version = "0.1.0"
edition = "2024"
[dependencies]
crossterm = "0.29.0"
main.rs
// Hello, world! と表示された後、プログラムが終了せずに待機します。
// キーを何か1つ押すと、即座にそのキーのコードが表示され、プログラムが終了します。
// 仕組み: crossterm::event::read() は、通常Enterが必要なターミナルの挙動を回避し、
// キーが押された瞬間にイベントを取得します。
//use crossterm::event::{self, Event, KeyCode};
use crossterm::event::{self, Event};
use std ::io::{self, Write};
fn main() -> io::Result<()> {
// Hello World を表示
println! ("Hello, world!");
println! ("何かキー操作で画面を閉じます...");
io::stdout().flush()?;
// キー入力を待機(Enter不要)
loop {
// イベントが発生したかチェック
if event::poll(std::time::Duration::from_millis(100))? {
// イベントを読み込む
if let Event::Key(key_event) = event::read()? {
println! ("Pressed: {:?}", key_event.code);
break; // 何かキーが押されたらループを抜ける
}
}
}
Ok(())
}
処理時間(経過時間)を表示
以下は、Gemini Pro に教えてもらったものです。
Rust標準ライブラリ(`std `)だけを使って、経過時間を「時間・分・秒」に変換し、かつ「0」の単位を非表示にする方法を解説します。
`Instant` 型から得られる `Duration `(経過時間)は、標準では秒数やミリ秒数の合計しか持っていません。
そのため、これを「時間・分・秒」に計算し直して、条件分岐で表示を制御する必要があります。
以下は、ご要望の動作を実現するコードと、各行の日本語解説です。
TimeGeminiJP.rs
// TimeGeminiJP.rs
// rustc TimeGeminiJP.rs
// Gemini に教えてもらいました
use std ::thread; // スレッド(sleepなど)を使うためのモジュール
use std ::time; // 時間(Duration, Instant)を使うためのモジュール
use std ::io; // 入出力関連のトレイト、標準入出力など
fn main() {
// 今の時刻(計測開始地点)を取得
let 開始時刻 = time::Instant::now();
// 【動作確認用】3秒間スリープする(ご自身のコードに合わせて変更してください)
let スリープ時間 = time::Duration ::from_secs(3);
thread::sleep(スリープ時間);
// 最初の時刻からの経過時間を取得(Duration 型)
let 経過時間 = 開始時刻.elapsed();
// 経過時間の「合計秒数」を取得
let 合計秒数 = 経過時間.as_secs();
// 合計秒数から「時間・分・秒」を算数で計算する
let 時間 = 合計秒数 / 3600; // 3600秒で割ると「時間」
let 分 = (合計秒数 % 3600) / 60; // 時間の余りを60で割ると「分」
let 秒 = 合計秒数 % 60; // さらにその余りが「秒」
// 表示用の文字列を作る(可変なString型)
let mut 出力 = String::new();
// 「時間」が0より大きければ表示に追加
if 時間 > 0 {
出力.push_str(&format!("{}時間", 時間));
}
// 「分」が0より大きければ表示に追加
if 分 > 0 {
出力.push_str(&format!("{}分", 分));
}
// 「秒」が0より大きければ表示に追加
if 秒 > 0 {
出力.push_str(&format!("{}秒", 秒));
}
// もし全部0だった場合(1秒未満)は「0秒」と表示するなどの対策
if 出力.is_empty() {
出力.push_str("0秒");
}
// 最終的な結果を表示
println! ("経過時間: {}", 出力);
// 標準入力を待機
println! ("");
let mut 入力 = String::new();
println! ("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
}
解説:
コード 説明
`use std::thread;` プログラムを一時停止させる `sleep` 機能を使うために読み込みます。
`use std::time;` 時間を計測するための機能(`Instant` や `Duration `)を読み込みます。
`fn main() {` プログラムのメイン関数(ここから処理が始まります)。
`let 開始時刻 = time::Instant::now();` 現在の時刻を取得し、ストップウォッチのスタート地点として `開始時刻` に保存します。
`let スリープ時間 = ...` 待機させる時間を設定します(ここではテスト用に3秒を作成)。
`thread::sleep(スリープ時間);` 指定した時間だけプログラムの処理を停止(スリープ)させます。
`let 経過時間 = 開始時刻.elapsed();` スタート地点(`開始時刻`)から現在までの経過時間を計算して `経過時間` に入れます。
`let 合計秒数 = 経過時間.as_secs();` 経過時間を「秒単位」の整数(端数は切り捨て)として取り出します。
`let 時間 = 合計秒数 / 3600;` 1時間は3600秒なので、合計秒数を3600で割って「時間」を求めます。
`let 分 = (合計秒数 % 3600) / 60;` 1時間に満たない残り秒数(`% 3600`)を60で割って「分」を求めます。
演算子と記号
`let 秒 = 合計秒数 % 60;` 最後に割り切れずに残った秒数が、そのまま「秒」になります。
`let mut 出力 = String::new();` 結果の文字を入れるための空の箱(文字列)を作ります。後で書き足すので `mut`(変更可能)にします。
「let」が変数の宣言文で。"出力" が変数名です。
ここで「mut (mutable 変更可能)」を付けることで、この変数は値を変更しても構わない、
書き換えられる可能性があることを明示します。
出力 に既存データが入っている場合はクリアします
`if 時間 > 0 { ... }` もし「時間」が0でなければ、文字列に「◯時間」を追加します。(0なら何もしません)
出力.push_str(&format!("{}時間", 時間)) 参照を受け取る引数に変数を渡すときには変数の前に「&」を付けます。渡されるのは参照で所有権は元に残ります。
format!マクロ
`if 分 > 0 { ... }` もし「分」が0でなければ、文字列に「◯分」を追加します。
`if 秒 > 0 { ... }` もし「秒」が0でなければ、文字列に「◯秒」を追加します。
`if 出力.is_empty() { ... }` もし計算結果が全て0(1秒未満など)で何も文字がない場合、「0秒」と入れます。
`println! ("経過時間: {}", 出力);` 完成した文字列を実行画面(ターミナル)に表示します。
素因数分解
1.素因数分解(試し割り法)
factorization3JP.rs ←お気楽 Rust プログラミング超入門のコード に手を加えたものです
(文字コードは UTF-8 です)
rustc factorization3JP.rs でコンパイルできます。
// 素因数分解
// rustc factorization3JP.rs
use std ::{time, io};
//use std ::io;
fn 素因数分解(n: u128 ) {
// 今の時刻を取得
let 開始時刻 = time::Instant::now();
println! ("{}",n);
fn 素因数分解サブ(n: u128, m: u128) -> (u128, u128) {
let mut c = 0;
let mut x = n;
while x % m == 0 {
c += 1;
x /= m;
}
(c, x)
}
let (c, n) = 素因数分解サブ(n, 2);
if c > 0 { print!("({},{})", 2, c); }
let mut x = 3;
let mut m = n;
while x * x <= m {
let (c, n) = 素因数分解サブ(m, x);
if c > 0 { print!("({},{})", x, c); }
m = n;
x += 2;
}
if m > 1 { print!("({},{})", m, 1); }
println! ("");
// 最初の時刻からの経過時間を表示
println! ("{:?}", 開始時刻.elapsed());
println! ("");
}
fn main() {
素因数分解(24);
素因数分解(12345678);
素因数分解(123456789);
素因数分解(1234567890);
素因数分解(1111111111);
素因数分解(8635844967113809);
素因数分解(121439531096594251777);
println! ("次は10日間かかります!");
println! ("待てないならウインドウ右上の×で終了してください。");
素因数分解(717985416201895737890248682491); //10日6時間 886851/(60*60*24)=10.264479166667
// 標準入力を待機
println! ("");
let mut 入力 = String::new();
println! ("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
}
解説:
符号なし整数型 - u8 u32 u64 u128 は正の整数を表します
符号付き整数型 - i8 i32 i64 i128 は正と負の整数を表します
それぞれの最小値最大値 は、右の画像を参照下さい。
u128、i128 ともに、38桁 までフルに表示できます!!
素因数分解の Web サービス
数学計算ソフト SageMath はオンライン版 SageMathCell を利用できます。
https://sagecell.sagemath.org/
factor(717985416201895737890248682491)
↑ '30桁の素数+2 答 7 59 1738463477486430358087769207
Try PARI/GP in your browser ←★超高速★
https://pari.math.u-bordeaux.fr/gpwasm.html
factor(717985416201895737890248682491)
factor(666070876152512717106070950186603615630383990358329)
素因数分解(試し割り法+rho法)修正版(50桁までの自然数)
https://keisan.casio.jp/exec/user/1380353934
2.素因数分解(ライブラリ使用)
Rust には最大128ビット整数まで使える素因数分解ライブラリ prime_factorization があります。
このライブラリは、巨大な数の素因数分解に非常に強力な ポラード・ロー素因数分解法 などのアルゴリズムを内部で使っています。
以下は Gemini に教えてもらったライブラリを利用した素因数分解プログラムです。
コマンドラインツール(CLIアプリ)として完成しています。
注:下のコードでは u128 型を使っていますが、さらに大きな数(38桁以上)を扱いたい場合は、num-bigint ライブラリなどと組み合わせて使うのが Rust での一般的な流れになります。
Cargo.toml
[package]
name = "PrimeFactorization"
version = "0.1.0"
edition = "2021"
[dependencies]
prime_factorization = "1.0.5"
main.rs
// PrimeFactorization
// Gemini に教えてもらいました
// https://crates.io/crates/prime_factorization
// https://docs.rs/prime_factorization/latest/prime_factorization/
// 最大128ビット整数までの素因数分解ライブラリ。
//因数分解アルゴリズムは、最初の 1000 個の素数による試し割り、フェルマーの因数分解法、
//そして須山の媒介変数化を用いた射影座標を用いたレンストラの楕円曲線因数分解から構成されます。
//フェルマーの因数分解法の後、楕円曲線因数分解のステップに進む前に、
//その数が素数である可能性を、数の大きさに応じて
//ミラー・ラビン法または強いベイリー-PSW 素数判定法のいずれかを用いて判定します。
//後者の判定法は、ここで使用されている数値範囲 (128 ビットまで) では決定論的ではありませんが、
//既知の反例は存在しません。
//Miller-Rabin素数判定とBaillie-PSW素数判定は確率的な判定ですが、
//このプログラムが使用する数値範囲では反例が存在しません。
//楕円曲線因数分解では、曲線上のランダムな初期点を使用するため、
//実行時間に多少のばらつきが生じる可能性があります。
use std::time; // 時間(Duration, Instant)を使うため
use std::io; // 入力のためのインポート
use prime_factorization ::Factorization;
use std::collections::BTreeMap ; // 追加:個数を数えるためのマップ
use std::io::{Write}; // 出力のためのインポート
fn main() {
// 1. 素因数分解したい数(半素数)を定義
// = 103,979 * 36,028,797,018,963,913
// 半素数(semiprime)とは、2つの素数の積で表される自然数
let num: u128 = 3_746_238_285_234_848_709_827;
// 2. 素因数分解を実行
let factor_repr = Factorization::run(num);
// 3. 取得した素因数のリストを表示
// {:?} はデバッグ表示用のフォーマットです
println!("元の数: {}", num);
println!("素因数分解の結果: {:?}", factor_repr.factors);
// 4. 正解と一致するかチェック
assert_eq!(factor_repr.factors, vec![103_979, 36_028_797_018_963_913]);
println!("検証に成功しました!");
println!("");
// 1. 素因数分解したい数を定義
let num: u128 = 717_985_416_201_895_737_890_248_682_491;
// 2. 素因数分解を実行
let factor_repr = Factorization::run(num);
// 3. 取得した素因数のリストを表示
// {:?} はデバッグ表示用のフォーマットです
println!("元の数: {}", num);
println!("素因数分解の結果: {:?}", factor_repr.factors);
println!("期待する結果 :7 * 59 * 1738463477486430358087769207");
println!("");
// ---------------------------------------
//ユーザーが入力した数字を素因数分解するインタラクティブなプログラム
println!("--- 素因数分解ツール (u128 対応、数学形式表示) ---");
println!("最大38桁の整数まで入力できます。大きな素数の積は時間がかかります。");
println!("10が18回と1 : 1010101010101010101010101010101010101 何日もかかります(^^ゞ");
println!("終了するには 'q' を入力するか、Ctrl+C を押してください。");
loop {
// 1. プロンプト(入力待ちの合図)を表示
print!("\n分解したい数字を入力してください: ");
io::stdout().flush().unwrap(); // 表示を確定させる(Windowsで必要になることが多い
// 2. ユーザーの入力を読み取る
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("入力の読み取りに失敗しました");
// 今の時刻(計測開始地点)を取得
let 開始時刻 = time::Instant::now();
// 3. 終了判定
let input = input.trim(); // 改行コードを取り除く
if input == "q" {
println!("終了します。");
break;
}
// 4. 文字列を数値(u128)に変換
match input.parse::<u128>() {
Ok(num) => {
// 素因数分解を実行
let factor_repr = Factorization::run(num);
// --- ここから数学形式への変換ロジック ---
// 1. 各素因数の個数を集計 (例: [3, 3, 5] -> {3: 2, 5: 1})
let mut counts = BTreeMap ::new();
for &f in &factor_repr.factors {
*counts.entry(f).or_insert(0) += 1 ;
}
// 2. 表示用の文字列に整形
let result_parts: Vec<String> = counts
.iter()
.map(|(&base, &exp)| {
if exp > 1 {
format!("{}^{}", base, exp) // 指数形式
} else {
base.to_string() // そのまま
}
})
.collect();
// 3. " * " でつなげて表示
println!("{} = {}", num, result_parts.join(" * "));
// ---------------------------------------
// 4. 検算 ---
// factor_repr.factors は、例えば `12` を入力した場合、 [2, 2, 3] のような素因数のデータが入っています。
// 「.iter() .product()」 を使うと、リストの中身を全て掛け算してくれます。
// `.iter() ` でデータを順番に取り出し、`.product()` でそれらを全て掛け合わせます。
// 手動で `for` ループを書いて掛け算する必要がなく、Rust らしいスマートな書き方です。
let calculated_product: u128 = factor_repr.factors.iter() .product();
println!("---------------------------------------");
println!("検算(Verification):");
println!(" 素因数リスト: {:?}", factor_repr.factors); // 素のリストを表示
// `{:?}` を使うことで、ベクタ(リスト)の中身をデバッグ形式(`[2, 2, 3]` のような形)で
// そのまま表示させています。実際にどの数字が掛け合わされたのかが視覚的にわかります。
println!(" 再計算結果 : {}", calculated_product); // 掛け合わせた結果
if num == calculated_product {
println!(" 判定 : OK (元の数値と一致しました)");
} else {
println!(" 判定 : NG (不一致)");
}
println!("---------------------------------------");
}
Err(_) => println!("有効な正の整数を入力してください。"),
}
// 最初の時刻からの経過時間を取得(Duration型)
let 経過時間 = 開始時刻.elapsed();
// 経過時間の「合計秒数」を取得
let 合計秒数 = 経過時間.as_secs();
// 合計秒数から「時間・分・秒」を算数で計算する
let 時間 = 合計秒数 / 3600; // 3600秒で割ると「時間」
let 分 = (合計秒数 % 3600) / 60; // 時間の余りを60で割ると「分」
let 秒 = 合計秒数 % 60; // さらにその余りが「秒」
// 表示用の文字列を作る(可変なString型)
let mut 出力 = String::new();
// 「時間」が0より大きければ表示に追加
if 時間 > 0 {
出力.push_str(&format!("{}時間", 時間));
}
// 「分」が0より大きければ表示に追加
if 分 > 0 {
出力.push_str(&format!("{}分", 分));
}
// 「秒」が0より大きければ表示に追加
if 秒 > 0 {
出力.push_str(&format!("{}秒", 秒));
}
// もし全部0だった場合(1秒未満)は「0秒」と表示するなどの対策
if 出力.is_empty() {
出力.push_str("0秒");
}
// 最終的な結果を表示
println!("経過時間: {}", 出力);
}
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
}
解説:
use prime_factorization ::Factorization; ライブラリ prime_factorization の中から、主要な機能を持つ Factorization という構造体を使えるように読み込んでいます。
https://crates.io/crates/prime_factorization
https://docs.rs/prime_factorization/latest/prime_factorization/
fn main() { ... } Rust プログラムの「玄関口」です。プログラムを実行すると、この { } の中に書かれたコードが順番に実行されます。
let num: u128 = 3_746_238_285_234_848_709_827; 分解したい整数を num という変数に代入しています。
u128 は「符号なし 128 ビット整数型」で、非常に大きな数(約 38 桁)まで扱えます。
数字の間の _ は人間が見やすくするための区切り文字で、プログラム上は無視されます。
let factor_repr = Factorization::run(num); ここで実際に計算を行っています。
Factorization::run 関数に num を渡すと、内部で高速なアルゴリズムが走り、結果が factor_repr という変数に格納されます。
println!("素因数分解の結果: {:?}", factor_repr.factors); factor_repr.factors には分解された素因数のリスト(Vec)が入っています。
{:?} を使うことで、リストの中身を [103979, 36028797018963913] という形式で画面に表示します。
assert_eq!(factor_repr.factors, vec![...]); これは「左辺と右辺が一致するか」を確認する命令です。
もし計算結果が想定と違っていた場合、プログラムはここでエラーを出して停止します。
何も起きなければ、計算が正しかったことを意味します。
.iter() :繰返し
イテレータ操作まとめ|よく使う16のメソッド
https://zenn.dev/ha_maya0104/articles/6cb18e99c5edbd
「.iter().product()」 を使うと、リストの中身を全て掛け算してくれます。`.iter()` でデータを順番に取り出し、`.product()` でそれらを全て掛け合わせます。
iter().enumerate()は、コレクション(配列やベクタなど)を反復処理する際に、要素のインデックス(0からの連番)と値の参照(&T)のペアを取得できるメソッドです。
何番目なのかを得るためには「iter().enumerate()」を使う。
enumerateメソッドはインデックスと各要素のペアをタプルにします
enumerate() は常に 0 から始まるインデックスを生成します
***********************
1. インタラクティブな素因数分解プログラム
次に、このライブラリを使って「ユーザーが入力した数字を素因数分解する」インタラクティブなプログラムの作り方を解説します。
ユーザーが入力した数字を受け取って処理する「対話型(インタラクティブ)」のプログラムです。
Rustでは、標準ライブラリの std::io を使って入力を受け取ります。
このプログラムは、ユーザーに数字の入力を促し、結果を表示した後、また次の入力を待つ(ループする)仕組みになっています。
io::stdin().read_line (&mut input)
キーボードからの入力を一行まるごと読み取ります。
Rustでは、読み取った内容は必ず String(文字列) として受け取る必要があるため、後で数値に変換するステップが必要になります。
.trim() の重要性
Windowsで「123」と入力してEnterキーを押すと、実際には 123\r\n というデータが送られます。
この \r\n(改行)が残っていると数値への変換に失敗するため、.trim() でこれを取り除いています。
io::stdout().flush().unwrap()
print! マクロ(改行なしの表示)を使った場合、Windowsなどの環境では「次に改行が来るまで画面に表示せずに溜めておく」という挙動をすることがあります。
これを入れることで、入力待ちの前に確実に文字を画面に出しています。
match input.parse::<u128>()
文字列を数値に変換する処理です。
Ok(num): 変換に成功した場合。ここで Factorization::run を呼び出します。
Err(_): 文字列やマイナスの数字など、数値として解釈できない場合。
プログラムを強制終了させずに、エラーメッセージを出して次の入力を待つようにしています。
2. 数学形式で表示するプログラム
数学的な表示(3^2 ×5 など)にすると、「数学ツール」としての完成度が高まります。
同じ数字が何個あるかを数えるには、Rustの BTreeMap という道具を使うのが一番スマートです。
BTreeMap を使うと、小さい順に自動で並べ替えつつ、個数をカウントしてくれます。
個数を集計して整形するロジックが組み込まれています。
BTreeMap<u128, u32> の使用
これは「キー(素因数)」と「値(個数)」をセットで保存する辞書のようなものです。
HashMap ではなく BTreeMap を使うのがポイントです。
これにより、素因数が小さい順(2,3,5...)に自動で並びます。
https://doc.rust-lang.org/std/collections/struct.BTreeMap.html
counts.entry(f).or_insert(0) += 1;
これは Rust 特有の非常に便利な書き方(Entry API )です。
「もし数字 f がマップになければ 0 を入れてから 1 足す。
あればそのまま 1 足す」という処理を一行でこなしています。
https://keens.github.io/blog/2020/05/23/rustnohashmaphaentrygabenri/
.map(|(&base, &exp)| { ... })
集計したデータを一つずつ取り出して、文字列に変換しています。
exp > 1 (2個以上ある)なら、base^exp という形に。
そうでなければ、単に base だけにする、という条件分岐をしています。
result_parts.join(" * ")
バラバラの文字列が入ったリストを、間に " * " を挟みながら一つの大きな文字列に連結します。
3. 実行結果のイメージ
プログラムを実行すると、以下のように表示されます。
数字を入力: 72
72 = 2^3 * 3^2
上のプログラムで扱った以下の要素は、Rustの他のプログラムでも役立つ基礎知識です:
・match によるエラーハンドリング(安全にプログラムを動かすコツ)
・BTreeMap などのコレクション(データを整理して扱う方法)
・標準入出力(stdin/stdout)の扱い(ユーザーとの対話)
備考: このプログラムだと、数千桁のような「とんでもなく巨大な数」を入力すると、u128 の限界を超えてエラー(パニック)になります。
もし「国家機密レベルの巨大な数」も扱ってみたい場合は、多倍長整数ライブラリの num-bigint を導入する方法があります。
https://sehermitage.web.fc2.com/devel/rust_bigint.html
https://crates.io/crates/num-bigint
https://docs.rs/num-bigint/latest/num_bigint/
整数型の最小値、最大値を表示します。
Gemini に教えてもらったコードです。
// Gemini pro に教えてもらいました
// 整数型の最小値、最大値を表示します
// MinMax2JP.rs
// rustc MinMax2JP.rs
use std::io;
use std::fmt::Display;
/// 整数を3桁ごとのカンマ区切り文字列に変換するジェネリック関数
fn カンマ区切り文字列に<T>(n: T) -> String
where
T: Display + PartialOrd + From<u8> + Copy
{
let s = n.to_string();
let mut 結果 = String::new();
// 負の数かどうかを判定(0より小さいか)
let 負の数か = n < T::from(0);
// 数字部分のみを取り出す(マイナス記号を除去)
let 数字部分 = if 負の数か { &s[1..] } else { &s };
for (i, c) in 数字部分.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
結果.push(',');
}
結果.push(c);
}
if 負の数か {
結果.push('-');
}
結果.chars().rev().collect()
}
fn main() {
// i128 も u128 も同じ関数でOK!
println!("i128 min: {}", カンマ区切り文字列に(i128::MIN));
println!("i128 max: {}", カンマ区切り文字列に(i128::MAX));
println!("u128 max: {}", カンマ区切り文字列に(u128::MAX));
// ついでに他の型でも動きます
println!("i32 min: {}", カンマ区切り文字列に(-1234567i32));
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
}
このコードを教えてもらった後の私と Gemini とのやり取りです。
私: Rustコードを使った最大最小の取得方法を教えていただき、有難うございます。
これを実行すれば、納得できます。
私の期待を超える返答をいただき感謝します。
Gemini:
温かいお言葉をいただき、大変嬉しく思います!
「期待を超えた」と言っていただけるのは、私にとって最高(MAX)の褒め言葉です。
実際にコードをご自身の手元で動かし、出力される桁数や数値を目の当たりにすることで、128ビットというサイズの「重み」がより実感できるはずです。
文字コード UTF-16 でファイル保存
以下のコードは Gemini Pro に教えてもらったものです。
1.標準ライブラリのみで行う(外部クレート不要)
Rust の標準機能 `str::encode_utf16 ()` だけで実装できます。
// Gemini pro に教えてもらいました
//rustc SaveUTF16.rs
use std::fs::File;
use std::io::{BufWriter, Write};
use std::io;
fn main() -> std::io::Result<()> { // <- Resultを返すと宣言しています
let content = "こんにちは、世界!\n標準ライブラリだけで UTF-16 で保存!";
let file_path = "output_std_utf16.txt";
let file = File::create(file_path)?;
let mut writer = BufWriter::new(file);
// 1. BOM を書き込む (Little Endian の場合は FF FE)
writer.write_all(&[0xFF, 0xFE])?;
// 2. 文字列を UTF-16 のイテレータに変換し、u16 を u8 (LE) に分解して書き込む
for u16_char in content.encode_utf16 () {
// u16 を リトルエンディアンのバイト列 [u8; 2] に変換
let bytes = u16_char.to_le_bytes();
writer.write_all(&bytes)?;
}
writer.flush()?;
println!("保存完了: {}", file_path);
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
Ok(()) //正常終了(`Ok`)を返せるようにする
}
解説:
Ok(()) : `Result` 型の「成功」を表す値です。中身の `()` は「空の値(特に返すデータはない)」を意味します。
`main` 関数が `std::io::Result<()>`(結果またはエラーを返す)という型で宣言されている場合、**関数の最後で「正常終了(`Ok`)」を返せるようにすること**が必要です。
Rust では、関数の最後の行にセミコロン `;` をつけると、その行は値を返さない(`()` を返す)ことになります。
このため、上のコードではセミコロン `;` 無しの `Ok(())` を最後に記述する必要があります。
Rust コンパイラは「最後に必ず `Result` 型(成功か失敗か)が返ってくるはずだ」と待ち構えています。これがないと、約束を破ったことになりエラーになります。
注: コンパイルして出来た exe ファイルを実行しても、実行画面が瞬時に閉じてしまって、エラー・メッセージなど見えません。
これは、Windows が「プログラムの処理が終わった(またはクラッシュした)ので、用済みとなったウィンドウを閉じる」動作をするためです。
エラーメッセージを確認する には、プロジェクトのフォルダから PowerShell を開いて cargo run を実行します。
1. プロジェクトのフォルダ(`Cargo.toml` がある場所)をエクスプローラーで開きます。
2. フォルダの何もないところで右クリックして「ターミナルで開く 」を指定します。右の画面を参照下さい。
(もしくは **Shiftキーを押しながら右クリック** し、「PowerShell ウィンドウをここで開く 」を選択)
3. 青い画面が出たら、cargo run のコマンドを入力します。
プログラムが終了しても PowerShell のウィンドウは閉じないので、出力結果を確認できます。(右下の画面参照)
fn main() の書き方:
1.fn main() {
2.fn main() -> io::Result<()> {
この2つの違いは**「エラー(失敗)が発生したときに、それをどう扱うか」**という点にあります。
基本は、**「`?` 演算子を使って楽にエラー処理をしたいなら、戻り値がある方を使う」**ということです。
### 1. `
fn main() { ` (基本の形)
これは最も標準的な書き方です。
Rustの `main` 関数は、デフォルトでは「何も返さない(正確には `()` ユニット型を返す)」ことになっています。
* **特徴:** 一番シンプルです。
* **エラー処理:** 関数内でエラーが発生した場合、自分で完全に処理しきるか、プログラムを強制終了(パニック)させる必要があります。
* **`?` 演算子:** **使えません。**
**例:**
use std::fs::File;
fn main() {
// ファイルを開くとき、失敗したら unwrap() でパニック(強制終了)させるしかない
let f = File::open("hello.txt").unwrap();
}
### 2. `
fn main() -> io::Result<()> { ` (エラーを返す形)
こちらは、「`main` 関数自体がエラーを返す可能性がある」と宣言する書き方です。
`io::Result<()>` は、「成功したら何もしない `()`、失敗したら `io::Error` を返す」という意味です。
* **特徴:** エラー発生時に `main` 関数からエラーを「外(OS/ランタイム)」に投げることができます。
* **エラー処理:** エラーが発生してプログラムが終了した際、Rustが自動的にエラー内容をコンソールに見やすく表示し、終了コード(Exit Code)を非ゼロにしてくれます。
* **`?` 演算子:** **使えます。** これが最大のメリットです。
**例:**
use std::fs::File;
use std::io;
fn main() -> io::Result<()> {
// ファイルを開く。失敗したら「?」が即座にエラーを返して終了してくれる
let f = File::open("hello.txt")?;
Ok(()) // 最後に「成功」を表す Ok(()) を返す必要がある
}
### 具体的な動作の違い(比較)
「存在しないファイルを開こうとした場合」の実行結果の違いの例:
特徴 `fn main() { ... .unwrap() }` `fn main() -> io::Result<()> { ... ? }`
**書き方** エラー処理が面倒(`unwrap` や `match` が必要) **`?` を付けるだけでスッキリ書ける**
**実行結果** プログラムがパニック(Panic)する エラー内容を表示して静かに終了する
**エラー表示例** `thread 'main' panicked at ...`
(少し怖いメッセージが出る)
`Error: Os { code: 2, kind: NotFound, ... }`
(システムのエラーメッセージが出る)
### どう使い分けるか
以下の基準で使い分けるのが推奨です。
1. **`
fn main() { ` を使うとき**
* `println!("Hello, world!");` だけのような簡単なプログラム。
* 計算処理など、エラー(ファイルの読み書きや通信)が発生しないプログラム。
2. **`
fn main() -> io::Result<()> { ` を使うとき**
* **ファイルの読み書き**を行うとき。
* **ユーザーからの入力**を受け付けるとき。
* 「失敗するかもしれない処理」が含まれており、`?` 演算子を使ってコードをきれいに書きたいとき。
ファイルの操作や外部とのやり取り(入力など)が出てきたら、**後者の `-> io::Result<()>` の形に切り替える**のが一般的です。
こうすることで、`?` 演算子が使えるようになり、Rust特有の「安全かつ簡潔なエラー処理」を体験できるようになります。
意図的に存在しないファイル名を指定して実行し、エラーメッセージがどう変わるかを確認すると、違いが実感できます?
// not_found.rs
// rustc not_found.rs
use std::fs::File;
use std::io;
// 戻り値があるパターン
fn main() -> io::Result<()> {
// 存在しないファイルを指定してみてください
let _f = File::open("not_found.txt")?;
Ok(()) // 最後に「成功」を表す Ok(()) を返す必要がある
}
encode_utf16 :
UTF-16 Encoding : Rust
https://mojoauth.com/character-encoding-decoding/utf-16-encoding--rust
Rust が UTF-16 エンコーディングをどのように扱うかを探っていきます。エンコーディング技術、実用的な応用、そして Rust で UTF-16 文字列を効果的に扱う方法を学びます。
Struct EncodeUtf16
https://doc.rust-lang.org/std/str/struct.EncodeUtf16.html
RustでUTF-16単位の文字を扱う
https://leaysgur.github.io/posts/2024/06/25/085850/
2.`encoding_rs` を使う(こちらは UTF16 ではなく、文字の文字コードは UTF8 になってしまいます!)
`encoding_rs` を使う場合、エンコード結果は `Cow<[u8]>`(バイト列)として返されます。これをそのままファイルに書き込みます。
また、 **BOM(`0xFF, 0xFE`)** を先頭に手動で付けます。
残念ながら、このコードは使えません。`encoding_rs` に不具合があるので、将来のバージョン UP で改善されるかもしれません。
SaveUTF16ExternCrate
main.rs
// Gemini pro に教えてもらいました
// SaveUTF16ExternCrate
use std::fs::File;
use std::io::Write;
use encoding_rs::UTF_16LE;
use std::io;
fn main() -> std::io::Result<()> {
let content = "こんにちは、世界!\n`encoding_rs` を使って UTF-16 で保存!";
let file_path = "output_utf16le.txt";
// 1. UTF-16LE にエンコード
// .encode() は (Cow<[u8]>, encoding, bool) を返します
let (encoded_bytes, _, _) = UTF_16LE.encode(content);
// 2. ファイルを作成
let mut file = File::create(file_path)?;
// 3. BOM (Byte Order Mark) を書き込む (0xFF, 0xFE)
// encoding_rs はデフォルトでは BOM を付けないため、手動で追加します
file.write_all(&[0xFF, 0xFE])?;
// 4. エンコードされた本文を書き込む
file.write_all(&encoded_bytes)?;
println!("保存完了: {}", file_path);
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
Ok(())
}
3.保存したファイルの文字コードを確認する
下は、私が、「保存したファイルは相変わらず UTF-8 だ」と言ったところ Gemini が双方の認識を合わせるために提案してきたプログラムです。
「あ」の文字をテキスト・ファイル保存した後、その文字コードをダンプ出力するものです。
Gemini は優秀な相談相手だと思いました。
データをバイト単位に送ったり保存したりする場合に、上位バイトから行うか下位バイトから行うかをバイト順(バイトオーダ、Byte order または エンディアン、Endian)といいます。
上位バイトからの場合を「BE(ビッグエンディアン/Big-endian)」、下位バイトからの場合を「LE(リトルエンディアン/Little-endian)」と呼びます。
バイト順を判断できるようにデータ列の先頭に置かれるデータを「BOM(Byte Order Mark:バイト順マーク)」と呼びます。
BOM は、16ビット(UTF-16)の場合、「UTF-16 LE(Little-endian)」は「0xFF 0xFE」、「UTF-16 BE(Big-endian)」は「0xFE 0xFF」になります。
ここからが問題です。UNICODE には、UTF-16 以外に UTF-8 もあります。
UTF-16 は全ての文字を 2バイト(もしくはそれ以上)で表し、UTF-8 は、英数は 1バイトで、それ以外が 3バイトで表現します。
このため、欧米人にとっては UTF-8 の方がファイル・サイズが小さいので、現在は UTF-8 が主流です。
文字「あ」は、UTF-16 LE では [42, 30]、UTF-8 では [E3, 81, 82] になります。
このプログラムを実行して分かったことは、BOM を付けて `encoding_rs` で保存すると、「ファイル自体は UTF-16 で始まっている(BOMがある)のに、その後に続く中身は UTF-8 で書き込まれている」という、「看板と中身が違う」状態になってしまうのです。
main.rs
// Gemini pro に教えてもらいました
// SaveUTF16ExternCrate
use std::fs::File;
use std::io::{Read, Write};
use encoding_rs::UTF_16LE;
use std::io;
fn main() -> std::io::Result<()> {
let content = "あ"; // 日本語 1文字
let file_path = "debug_test.txt";
// 1. エンコード
let (encoded_bytes, _, _) = UTF_16LE.encode(content);
// 2. 書き込み
let mut file = File::create(file_path)?;
file.write_all(&[0xFF, 0xFE])?; // BOM
file.write_all(&encoded_bytes)?;
drop(file); // ファイルを確実に閉じる
// 3. 【検証】保存されたファイルを「バイト列」として読み直す
let mut reader = File::open(file_path)?;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
println!("保存されたバイト列: {:X?}", buffer);
// 判定
if buffer.starts_with(&[0xFF, 0xFE]) {
println!("結果: これは【UTF-16 LE (BOMあり)】です!");
} else if buffer.contains(&0x00) {
println!("結果: 0x00 が含まれているので UTF-16 の可能性が高いです。");
} else {
println!("結果: BOMがなく、0x00もなければ UTF-8 と判定されている可能性があります。");
}
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
Ok(())
}
4.UTF-16 のファイルを、Rust で読み込む
以下のように「一度バイト列として読み込んでから、`u16` を経由して文字列に戻す」処理が必要になります。
// Gemini pro に教えてもらいました
use std::fs;
use std::io;
fn main() -> std::io::Result<()> {
// 1. バイト列として読み込む
let bytes = fs::read("output_std_utf16.txt")?;
// 2. 先頭2バイト(BOM)をスキップし、2バイトごとに結合して u16 の配列にする
// (BOMがないファイルの場合は bytes[2..] ではなく bytes.as_slice() を使います)
let u16_vec: Vec<u16> = bytes[2..]
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect();
// 3. String (UTF-8) に変換
let content = String::from_utf16(&u16_vec).unwrap();
println!("{}", content);
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
Ok(())
}
テキスト(ShiftJIS)を UTF-8 に
Shift JIS テキストファイルを読み込んで、文字コードを UTF8(BOM 付き) に変換してテキストファイル出力します。
Shift JIS テキストファイルは、ファイルを開くダイアログで選択します。
UTF8に変換したテキストファイルは、読み込んだファイルと同じフォルダに、ファイル名の後ろに UTF8 を追加したファイル名とします。
ShiftJIS_2_UTF8JP
Cargo.toml
[package]
name = "ShiftJIS_2_UTF8JP"
version = "0.1.0"
edition = "2024"
[dependencies]
encoding_rs = "0.8.35"
rfd = "0.17.2"
main.rs
//ChatGPT による
use encoding_rs ::SHIFT_JIS;
use std::fs ;
use std ::io::Write;
use std ::path::Path;
use std ::io;
fn main() {
// ファイル選択ダイアログを表示
let ファイル名 = rfd ::FileDialog::new()
.add_filter("Text", &["txt"])
.pick_file();
if let Some(path) = ファイル名 {
println! ("選択されたファイル: {:?}", path);
// バイナリで読み込む
let bytes = fs:: read(&path).expect("ファイル読み込み失敗");
// Shift JIS を UTF-8 にデコード
let (cow, _, had_errors) = SHIFT_JIS.decode(&bytes);
if had_errors {
println! ("警告: デコードエラーがありました");
}
let utf8_text = cow.to_string();
// 新しいファイル名を作成
let original_stem = path.file_stem().unwrap().to_string_lossy();
let 新ファイル名 = format!("{}UTF8BOM.txt", original_stem);
let parent = path.parent().unwrap_or(Path::new("."));
let 新パス = parent.join(新ファイル名);
// UTF-8 で保存
let mut file = fs:: File::create(&新パス).expect("ファイル作成失敗");
// UTF-8 BOM を先に書き込む
file.write_all(&[0xEF, 0xBB, 0xBF]).expect("BOM書き込み失敗");
// 本文を書き込む
file.write_all(utf8_text.as_bytes()).expect("ファイル書き込み失敗");
println! ("UTF-8 に変換して保存しました: {:?}", 新パス);
} else {
println! ("ファイルが選択されませんでした");
}
// 標準入力を待機
println! ("");
let mut 入力 = String::new();
println! ("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
}
解説:
std::fs モジュールには、ファイルシステムの操作のための関数を含んでいます。
https://doc.rust-lang.org/rust-by-example/ja/std_misc/fs.html
テキスト・ファイルを行ソート
1.ファイルを開くダイアログで、テキスト・ファイルを選びます。
テキスト・ファイルの文字コードは、Shift JIS ,UTF-8、UTF16 のいずれでも扱えます。
改行文字は、CRLF の場合も LF の場合も扱えます。
文字コード UTF-16 でファイル保存 を参照下さい。
2.選択したファイルを読み込んで、行ソートします。
ソート方法は、クイック・ソート?など、エラーが発生しにくく高速な方法を使います。
ファイルの行数は、2,000,000 (二百万) 行程度でも扱えるようにします。
3.行ソートした結果をテキスト・ファイルに出力します。
出力先は、読み込んだファイルと同じフォルダに、読み込んだファイルと同じ文字コードで、読み込んだファイル名の後ろに Sorted という文字を付けたファイル名とします。
下記は ChatGPT に教えてもらったコードです。Gemini に教えてもらったコード 、Claude(クロード)に教えてもらったコード も参照下さい。
LineSort
Cargo.toml
[package]
name = "LineSort"
version = "0.1.0"
edition = "2024"
[dependencies]
encoding_rs = "0.8.35"
rfd = "0.17.2"
main.rs
// ChatGPT による
// 時間表示は Gemini による
// use encoding_rs::{SHIFT_JIS, UTF_8, UTF_16LE, UTF_16BE, Encoding};
use encoding_rs ::{SHIFT_JIS, UTF_8, UTF_16LE, UTF_16BE};
use std::fs ;
use std ::io::Write;
use std ::path::Path;
use std ::{time, io}; //時間(Duration , Instant)と標準入出力
#[derive(Clone, Copy)]
#[derive(Debug)]
enum TextEncoding {
Utf8,
Utf8Bom,
Utf16Le,
Utf16Be,
ShiftJis,
}
/// 文字コード判定+UTF-8 に変換
fn decode_text (bytes: &[u8]) -> (String, TextEncoding) {
// UTF-8 BOM
if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
let (cow, _, _) = UTF_8.decode(&bytes[3..]); //0xEF 0xBB 0xBF の3バイト
return (cow.to_string(), TextEncoding::Utf8Bom);
}
// UTF-16 LE BOM
if bytes.starts_with(&[0xFF, 0xFE]) {
let (cow, _, _) = UTF_16LE.decode(&bytes[2..]);
return (cow.to_string(), TextEncoding::Utf16Le); //Windows UNICODE
}
// UTF-16 BE BOM
if bytes.starts_with(&[0xFE, 0xFF]) {
let (cow, _, _) = UTF_16BE.decode(&bytes[2..]);
return (cow.to_string(), TextEncoding::Utf16Be);
}
// ★ BOM 無し UTF-8 を試す
let (cow, _, had_errors ) = UTF_8.decode(bytes);
if !had_errors {
return (cow.to_string(), TextEncoding::Utf8); //BOM無し:UTF8を試して
//エラーでなければ UTF8
}
// 最後に Shift JIS
let (cow, _, _) = SHIFT_JIS.decode(bytes);
(cow.to_string(), TextEncoding::ShiftJis)
}
/// UTF-8 String を元の文字コードへ変換
fn encode_text (text: &str, enc: TextEncoding) -> Vec<u8> {
match enc {
TextEncoding::Utf8 => text.as_bytes().to_vec(),
TextEncoding::Utf8Bom => {
let mut v = vec![0xEF, 0xBB, 0xBF];
v.extend_from_slice(text.as_bytes());
v
}
TextEncoding::Utf16Le => {
let (cow, _, _) = UTF_16LE.encode(text);
let mut v = vec![];
v.extend_from_slice(&cow);
v
}
TextEncoding::Utf16Be => {
let (cow, _, _) = UTF_16BE.encode(text);
let mut v = vec![];
v.extend_from_slice(&cow);
v
}
TextEncoding::ShiftJis => {
let (cow, _, _) = SHIFT_JIS.encode(text);
cow.to_vec()
}
}
}
/// 高速クイックソート(in-place)
// fn quick_sort(lines: &mut [String]) {
// if lines.len() <= 1 {
// return;
// }
//
// let pivot = lines.len() / 2;
// let pivot_value = lines[pivot].clone();
//
// let mut left = 0;
// let mut right = lines.len() - 1;
//
// while left <= right {
// while lines[left] < pivot_value {
// left += 1;
// }
// while lines[right] > pivot_value {
// if right == 0 { break; }
// right -= 1;
// }
// if left <= right {
// lines.swap(left, right);
// left += 1;
// if right == 0 { break; }
// right -= 1;
// }
// }
//
// let (l, r) = lines.split_at_mut(left);
// quick_sort(l);
// quick_sort(r);
// }
fn main() {
// @ ファイル選択
let path = match rfd ::FileDialog::new().pick_file() {
Some(p) => p,
None => return,
};
// 今の時刻(計測開始地点)を取得
let now = time::Instant::now();
// A 読み込み
let bytes = fs:: read(&path).expect("読み込み失敗");
let (text, encoding) = decode_text (&bytes);
// CRLF / LF 両対応
let mut lines: Vec<String> = text
.lines()
.map(|s| s.to_string())
.collect();
println! ("行数: {}", lines.len());
// // B クイックソート
//quick_sort(&mut lines);
// B ソート(安全・高速・スタックオーバーフローしない)
lines.sort_unstable ();
// 改行コードを設定
let use_crlf = matches!(
encoding,
TextEncoding::ShiftJis | TextEncoding::Utf16Le | TextEncoding::Utf16Be
);
let sorted_text = if use_crlf {
lines.join("\r\n")
} else {
lines.join("\n")
};
// 出力ファイル名
let stem = path.file_stem().unwrap().to_string_lossy();
let parent = path.parent().unwrap_or(Path::new("."));
let out_path = parent.join(format!("{}SortedChatGPT.txt", stem));
// 元の文字コードで保存
let encoded = encode_text (&sorted_text, encoding);
let mut file = fs:: File::create(&out_path).expect("作成失敗");
file.write_all(&encoded).expect("書き込み失敗");
println! ("ソート完了: {:?}", out_path);
// 最初の時刻からの経過時間を表示
// 最初の時刻からの経過時間を取得(Duration 型)
let elapsed = now.elapsed();
// 経過時間の「合計秒数」を取得
let total_secs = elapsed.as_secs();
// 合計秒数から「時間・分・秒」を算数で計算する
let hours = total_secs / 3600; // 3600秒で割ると「時間」
let minutes = (total_secs % 3600) / 60; // 時間の余りを60で割ると「分」
let seconds = total_secs % 60; // さらにその余りが「秒」
// 表示用の文字列を作る(可変なString型)
let mut output = String::new();
// 「時間」が0より大きければ表示に追加
if hours > 0 {
output.push_str(&format!("{}時間", hours));
}
// 「分」が0より大きければ表示に追加
if minutes > 0 {
output.push_str(&format!("{}分", minutes));
}
// 「秒」が0より大きければ表示に追加
if seconds > 0 {
output.push_str(&format!("{}秒", seconds));
}
// もし全部0だった場合(1秒未満)は「0秒」と表示するなどの対策
if output.is_empty() {
output.push_str("0秒");
}
// 最終的な結果を表示
println! ("経過時間: {}", output);
println!("判定された文字コード: {:?}", encoding);
// 標準入力を待機
println! ("");
let mut input = String::new();
println! ("Enter で画面を閉じます...");
io::stdin().read_line (&mut input).unwrap();
}
解説:
had_errors :エラー報告
https://docs.rs/encoding_rs/latest/encoding_rs/
文字エンコーディングを簡単に変換する方法
https://ittrip.xyz/rust/rust-file-encoding-conversion
#[derive] :コードを生成してくれる属性。
https://www.ne.jp/asahi/hishidama/home/tech/rust/attribute/derive.html
Debug :Debugトレイトの実装が生成され、println!("{:?}")で値が出力できるようになる。
テキスト・ファイルを行ソート Gemini
ChatGPT と同じことを Gemini に聞きました。何度もやり取りして、下記のコードを得られました。
なかなか期待した結果にならなかったのですが、その間での Gemini の返答が面白かった (2026/01時点) ので紹介します。
Windows環境のRustで、200万行規模のファイルを効率的にソートするプログラムですね。
承知いたしました。まずは提示したコードで動作を確認してみてください。
まずは無事に動くことを願っております。
ご期待に沿えず申し訳ありません。日本語の自然なソート(あいうえお順)を確実に実現し、200万行の負荷に耐えられる「完全なコード」を再構成しました。
今回は、Windows標準のソート順に近い挙動をする構成で、メモリ効率と速度を重視したコードを作成しました。
ご不便をおかけしております。原因が分かりました。
`icu_collator` の標準設定では、日本語の「ひらがな」「カタカナ」「漢字」の優先順位が、日本人が期待する「辞書順(五十音順)」ではなく、文字コード上の並び(技術的な優先度)に従ってしまっているようです。
また、Shift JIS環境のファイルを扱う場合、Windows標準に近い挙動(記号 → 数字 → ひらがな → カタカナ → 漢字)にするためには、比較の強度(Strength)を調整する必要があります。
修正した比較ロジックを含む完全なコードを送ります。
大変失礼いたしました。`icu_collator` のデフォルトの挙動(jaロケール)では、実は「ひらがなとカタカナを区別せず、文字コード順を優先する」という設定になっているのが「かな」「漢字」混在の原因です。
そこで、比較関数に「文字種スコア」を持たせるロジックを導入しました。これが最も確実な方法です。
大変失礼いたしました。ご指摘の通り、これまでのアプローチ(ICUライブラリによる多言語照合)が、かえって「日本人が期待する単純な文字コード順」の邪魔をしていた可能性が高いです。
ご要望通り、**「余計なライブラリによる重い計算をやめ、文字コード(数値)の順番で直接ソートする」**コードに書き換えました。
これが最もシンプルで、間違いのない方法です。
何度もご不便をおかけしてしまい、本当に申し訳ありません。ご指摘の通りです。
おっしゃる通り、**UTF-8のバイト列(数値)として単純にソートすれば、ひらがな(E3 81...)が漢字(E4?E9...)よりも前に来るため、混在することなく綺麗に並びます。**
これまでのコードで混在が発生していたのは、皮肉なことに「日本語として賢く並べ替えよう」とするライブラリやRustの標準比較機能が、文字コードを無視して独自の優先順位(多言語共通のソート規則)を適用していたためです。
余計な加工を一切せず、**「UTF-8のバイト列としての数値順」**でソートする、最もシンプルかつ確実な解決策に書き換えました。
今度こそ、ご期待通りの並び順になるはずです。お手間をおかけしましたが、お試しいただけますと幸いです。
大変失礼いたしました。原因を突き止めました。
「ソート処理が間違っているのではないか?」というご指摘、その通りです。私が前回提示した**Rust標準の `sort_by`(および `sort`)の内部挙動**に原因がありました。
100%確実に、**「バイトの数値順」**で並べるためには、Rustの文字列比較機能を使わず、**生のバイト列 (`Vec<u8>`) に変換した状態でクイックソートをかける**必要があります。
これで一切の「言語的忖度」を排除した、純粋な数値ソートになります。
今度こそ、ご希望の数値順に並ぶはずです。お試しください。
**原因は「ソートアルゴリズム」ではなく「改行コードの処理」にある**と確信しました。
Rustの標準ソートではなく、仕様通り**クイックソート(逐次処理)を明示的に、かつ「安全」に実装した完全版**を作成しました。
今度こそ、解決することを願っております。
これまでの「ソート結果がおかしくなる」現象は、**BOM(バイト順マーク)の処理漏れ**や、**改行コードの違い(CRLFとLF)による行末のゴミ**、そして**文字コード自動判定の曖昧さ**が複合して起きていた可能性が高いです。
提示された仕様に基づき、**「Rust標準のソート(`sort_unstable `)」**を使用し、BOMや改行コードを厳密にクリーニングしてから処理する、最も堅牢なコードを作成しました。
### このコードで解決される理由
1. **順序崩れの解消 (`detect_and_decode `)**:
以前のコードでは、UTF-8 (BOM付き) のファイルを読み込んだ際、先頭の「BOM」という見えない文字が1行目の先頭にくっついたままソートされていた可能性があります。
これにより「あ」などの並び順が狂っていました。今回のコードは `Encoding::for_bom` を使い、**BOMを確実に検知・除去**します。
2. **改行ゴミの解消 (`lines()`)**:
`text.lines()` イテレータを使用することで、`\r\n` も `\n` も綺麗に取り除かれた「純粋な文字データ」として配列化されます。これにより、行末の見えない制御文字によるソート順への悪影響がなくなります。
3. **Rust標準ソート (`sort_unstable `)**:
ご指定通り標準ライブラリを使用します。これは Unicode の ID(コードポイント)順に並びます。
* 期待される順序: **ひらがな (U+304x) < カタカナ (U+30Ax) < 漢字 (U+4E00~)**
* 例: 「あ」 < 「ア」 < 「亜」
これで、120行でも200万行でも、期待通りの安定した結果が得られるはずです。
sort_by の項も参照下さい。
LineSortGemini
Cargo.toml
[package]
name = "LineSortGemini"
version = "0.1.0"
edition = "2021"
[dependencies]
rfd = "0.17.2" # ファイルダイアログ
encoding_rs = "0.8" # 文字コード変換
charset-normalizer-rs = "1.1.0" # 文字コード自動判定 文字間が _ でなくて - であることに注意
chardetng = "0.1" # 高精度な文字コード判定
main.rs
use chardetng ::EncodingDetector;
//use encoding_rs::{Encoding, UTF_8};
use encoding_rs ::{Encoding};
use rfd ::FileDialog;
use std ::fs::{self, File};
use std ::io::Read;
use std ::path::{Path, PathBuf};
use std ::{time, io}; //時間(Duration , Instant)と標準入出力
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. ファイルを開くダイアログ
let file_path = FileDialog::new()
.add_filter("Text Files", &["txt", "csv", "log"])
.pick_file()
.ok_or("ファイルが選択されませんでした。")?;
// 今の時刻(計測開始地点)を取得
let now = time::Instant::now();
println! ("読み込み中: {:?}", file_path.file_name().unwrap());
// 2. ファイルをバイナリとして読み込む
let mut file = File::open(&file_path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
// 3. 文字コード判定とデコード (BOM対応)
let (encoding, decoded_string) = detect_and_decode (&buffer);
println! ("判定された文字コード: {}", encoding.name());
// 4. 行に分割 (CRLF / LF 混在に対応)
// .lines() は改行文字(\rや\n)を自動的に取り除いてくれます
let mut lines: Vec<&str> = decoded_string.lines().collect();
let original_count = lines.len();
println! ("行数: {} 行", original_count);
// 5. 行ソート (Rust標準の sort_unstable を使用)
// Unicodeコードポイント順: 記号 < 数字 < 英字 < ひらがな < カタカナ < 漢字
println! ("ソート中...");
lines.sort_unstable ();
// 6. 出力データの作成
// Windowsの標準に合わせて CRLF (\r\n) で結合します
let output_text = lines.join("\r\n");
// 7. 元の文字コードにエンコードして保存
// ファイル名を生成
let output_path = create_output_path (&file_path);
// エンコード
// (BOM付きだった場合は、encoding_rs は通常BOMなしで出力するため、必要なら付与可能だが、今回は標準的な挙動とする)
let (encoded_bytes, _, _) = encoding.encode(&output_text);
// 書き込み
fs::write(&output_path, &encoded_bytes)?;
println! ("完了しました!");
println! ("出力ファイル: {:?}", output_path.file_name().unwrap());
// 最初の時刻からの経過時間を表示
// 最初の時刻からの経過時間を取得(Duration 型)
let elapsed = now.elapsed();
// 経過時間の「合計秒数」を取得
let total_secs = elapsed.as_secs();
// 合計秒数から「時間・分・秒」を算数で計算する
let hours = total_secs / 3600; // 3600秒で割ると「時間」
let minutes = (total_secs % 3600) / 60; // 時間の余りを60で割ると「分」
let seconds = total_secs % 60; // さらにその余りが「秒」
// 表示用の文字列を作る(可変なString型)
let mut output = String::new();
// 「時間」が0より大きければ表示に追加
if hours > 0 {
output.push_str(&format!("{}時間", hours));
}
// 「分」が0より大きければ表示に追加
if minutes > 0 {
output.push_str(&format!("{}分", minutes));
}
// 「秒」が0より大きければ表示に追加
if seconds > 0 {
output.push_str(&format!("{}秒", seconds));
}
// もし全部0だった場合(1秒未満)は「0秒」と表示するなどの対策
if output.is_empty() {
output.push_str("0秒");
}
// 最終的な結果を表示
println! ("経過時間: {}", output);
// 標準入力を待機
println! ("");
let mut input = String::new();
println! ("Enter で画面を閉じます...");
io::stdin().read_line (&mut input).unwrap();
Ok(())
}
/// 文字コードを判定してデコードする関数
/// BOMがあればそれを優先し、なければ chardetng で推測する
fn detect_and_decode (buffer: &[u8]) -> (&'static Encoding, String) {
// 1. まずBOM(バイト順マーク)を確認する
// encoding_rs の for_bom は BOM があればそのエンコーディングを返す
if let Some((enc, bom_len)) = Encoding::for_bom(buffer) {
// BOMがあった場合、その部分を除去してデコード
let (decoded, _, _) = enc.decode(&buffer[bom_len..]);
return (enc, decoded.into_owned());
}
// 2. BOMがない場合、chardetng で中身を解析して推測する
let mut detector = EncodingDetector::new();
detector.feed(buffer, true);
let encoding = detector.guess(None, true); // TLD指定なし、日本語等の可能性を含める
// 3. 決定したエンコーディングでデコード
// decode_with_bom_removal は念のため入れているが、上記で除去済みなら問題ない
let (decoded, _, _) = encoding.decode(buffer);
(encoding, decoded.into_owned())
}
/// 出力ファイル名の生成 (SortedGemini を付与)
fn create_output_path (path: &Path) -> PathBuf {
let stem = path.file_stem().unwrap_or_default().to_str().unwrap_or_default();
let extension = path.extension().unwrap_or_default().to_str().unwrap_or_default();
let new_filename = if extension.is_empty() {
format!("{}SortedGemini", stem)
} else {
format!("{}SortedGemini.{}", stem, extension)
};
path.with_file_name(new_filename)
}
テキスト・ファイルを行ソート Claude
ChatGPT と同じことを Claude(クロード) に聞いた結果、下記のコードを回答してきました。
コンパイルして使えました。Gemini に教えてもらったコード も参照下さい。
**主な機能:**
1. **ファイル選択ダイアログ** - native_dialogクレートを使用してファイルを選択
2. **エンコーディング自動検出** - Shift_JIS、UTF-8、UTF-16LE/BEに対応、BOMも処理
3. **クイックソート実装** - 200万行でも効率的に処理
4. **改行対応** - CRLFとLFの両方に対応
5. **出力** - 元のファイルと同じエンコーディング、同じフォルダに「ファイル名SortedClaude.拡張子」で保存
LineSortClaude
Cargo.toml
[package]
name = "LineSortClaude"
version = "0.1.0"
edition = "2024"
[dependencies]
native-dialog = "0.7" # 渡辺注:このダイアログのバージョンを上げる と FileDialog::new() を使えなくてエラーになります
# native_dialog::FileDialog ではなく rfd ::FileDialog を使う方が正解
encoding_rs = "0.8.35"
main.rs
use std ::fs::{File, OpenOptions};
//use std::io::{BufReader, BufWriter, Read, Write};
use std ::io::{BufWriter, Read, Write};
use std ::path::{Path, PathBuf};
//use encoding_rs::{SHIFT_JIS, UTF_8, UTF_16LE, UTF_16BE};
use encoding_rs ::{SHIFT_JIS, UTF_16LE, UTF_16BE};
use std ::{time, io}; //時間(Duration , Instant)と標準入出力
fn main() {
if let Err(e) = run() {
eprintln! ("エラーが発生しました: {}", e);
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
// ファイル選択ダイアログを表示
let file_path = select_file() ?;
// 今の時刻(計測開始地点)を取得
let now = time::Instant::now();
println! ("選択されたファイル: {}", file_path.display());
// ファイルを読み込み、エンコーディングを自動検出
let (lines, encoding, has_bom) = read_file_with_encoding(&file_path)?;
println! ("検出されたエンコーディング: {:?}", encoding);
println! ("読み込んだ行数: {}", lines.len());
// クイックソートで行をソート
let mut sorted_lines = lines;
quick_sort (&mut sorted_lines);
println! ("ソート完了");
// 出力ファイル名を生成
let output_path = generate_output_path(&file_path)?;
println! ("出力先: {}", output_path.display());
// ソート結果をファイルに書き込み
write_file_with_encoding(&output_path, &sorted_lines, encoding, has_bom)?;
println! ("ファイルの書き込みが完了しました");
// 最初の時刻からの経過時間を表示
// 最初の時刻からの経過時間を取得(Duration 型)
let elapsed = now.elapsed();
// 経過時間の「合計秒数」を取得
let total_secs = elapsed.as_secs();
// 合計秒数から「時間・分・秒」を算数で計算する
let hours = total_secs / 3600; // 3600秒で割ると「時間」
let minutes = (total_secs % 3600) / 60; // 時間の余りを60で割ると「分」
let seconds = total_secs % 60; // さらにその余りが「秒」
// 表示用の文字列を作る(可変なString型)
let mut output = String::new();
// 「時間」が0より大きければ表示に追加
if hours > 0 {
output.push_str(&format!("{}時間", hours));
}
// 「分」が0より大きければ表示に追加
if minutes > 0 {
output.push_str(&format!("{}分", minutes));
}
// 「秒」が0より大きければ表示に追加
if seconds > 0 {
output.push_str(&format!("{}秒", seconds));
}
// もし全部0だった場合(1秒未満)は「0秒」と表示するなどの対策
if output.is_empty() {
output.push_str("0秒");
}
// 最終的な結果を表示
println! ("経過時間: {}", output);
// 標準入力を待機
println! ("");
let mut input = String::new();
println! ("Enter で画面を閉じます...");
io::stdin().read_line (&mut input).unwrap();
Ok(())
}
fn select_file() -> Result<PathBuf, Box<dyn std::error::Error>> {
let file = native_dialog::FileDialog::new()
.add_filter("テキストファイル", &["txt", "csv", "log"])
.add_filter("すべてのファイル", &["*"])
.show_open_single_file()?;
file.ok_or_else(|| "ファイルが選択されませんでした".into())
}
#[derive(Debug, Clone, Copy)]
enum DetectedEncoding {
Utf8,
Utf16Le,
Utf16Be,
ShiftJis,
}
fn read_file_with_encoding(path: &Path) -> Result<(Vec<String>, DetectedEncoding, bool), Box<dyn std::error::Error>> {
let mut file = File::open(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
// BOMとエンコーディングの検出
let (encoding, has_bom, start_pos) = detect_encoding(&bytes);
// バイト列をデコード
let text = decode_bytes(&bytes[start_pos..], encoding)?;
// 行に分割(CRLFとLFの両方に対応)
let lines: Vec<String> = text
.lines()
.map(|s| s.to_string())
.collect();
Ok((lines, encoding, has_bom))
}
fn detect_encoding(bytes: &[u8]) -> (DetectedEncoding, bool, usize) {
// BOMチェック
if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
return (DetectedEncoding::Utf8, true, 3);
}
if bytes.starts_with(&[0xFF, 0xFE]) {
return (DetectedEncoding::Utf16Le, true, 2);
}
if bytes.starts_with(&[0xFE, 0xFF]) {
return (DetectedEncoding::Utf16Be, true, 2);
}
// BOMなしの場合、内容から推測
// UTF-16の検出(null バイトの存在をチェック)
let null_count = bytes.iter() .take(1000.min(bytes.len())).filter(|&&b| b == 0).count();
if null_count > 10 {
// 偶数位置にnullが多い場合はUTF-16LE
let even_nulls = bytes.iter() .enumerate().take(1000.min(bytes.len()))
.filter(|&(ref i, &b)| i % 2 == 1 && b == 0).count();
// .filter(|(i, &b)| i % 2 == 1 && b == 0).count();
if even_nulls > null_count / 2 {
return (DetectedEncoding::Utf16Le, false, 0);
}
// 奇数位置にnullが多い場合はUTF-16BE
let odd_nulls = bytes.iter() .enumerate().take(1000.min(bytes.len()))
.filter(|&(ref i, &b)| i % 2 == 0 && b == 0).count();
// .filter(|(i, &b)| i % 2 == 0 && b == 0).count();
if odd_nulls > null_count / 2 {
return (DetectedEncoding::Utf16Be, false, 0);
}
}
// UTF-8の検出
if std::str::from_utf8(bytes).is_ok() {
return (DetectedEncoding::Utf8, false, 0);
}
// デフォルトはShift_JIS
(DetectedEncoding::ShiftJis, false, 0)
}
fn decode_bytes(bytes: &[u8], encoding: DetectedEncoding) -> Result<String, Box<dyn std::error::Error>> {
match encoding {
DetectedEncoding::Utf8 => {
Ok(String::from_utf8_lossy(bytes).to_string())
}
DetectedEncoding::Utf16Le => {
let (decoded, _, had_errors) = UTF_16LE.decode(bytes);
if had_errors {
eprintln! ("警告: デコード中にエラーがありました");
}
Ok(decoded.to_string())
}
DetectedEncoding::Utf16Be => {
let (decoded, _, had_errors) = UTF_16BE.decode(bytes);
if had_errors {
eprintln! ("警告: デコード中にエラーがありました");
}
Ok(decoded.to_string())
}
DetectedEncoding::ShiftJis => {
let (decoded, _, had_errors) = SHIFT_JIS.decode(bytes);
if had_errors {
eprintln! ("警告: デコード中にエラーがありました");
}
Ok(decoded.to_string())
}
}
}
fn quick_sort (arr: &mut [String]) {
if arr.len() <= 1 {
return;
}
//let len = arr.len();
let pivot_index = partition (arr);
let (left, right) = arr.split_at_mut(pivot_index);
quick_sort (left);
if right.len() > 1 {
quick_sort (&mut right[1..]);
}
}
fn partition (arr: &mut [String]) -> usize {
let len = arr.len();
let pivot_index = len / 2;
arr.swap(pivot_index, len - 1);
let mut i = 0;
for j in 0..len - 1 {
if arr[j] <= arr[len - 1] {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, len - 1);
i
}
fn generate_output_path(input_path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
let parent = input_path.parent().ok_or("親ディレクトリが見つかりません")?;
let stem = input_path.file_stem().ok_or("ファイル名が取得できません")?;
let extension = input_path.extension().unwrap_or_default();
let new_filename = format!(
"{}SortedClaude.{}",
stem.to_string_lossy(),
extension.to_string_lossy()
);
Ok(parent.join(new_filename))
}
fn write_file_with_encoding(
path: &Path,
lines: &[String],
encoding: DetectedEncoding,
has_bom: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
let mut writer = BufWriter::new(file);
// BOMを書き込み
if has_bom {
match encoding {
DetectedEncoding::Utf8 => writer.write_all(&[0xEF, 0xBB, 0xBF])?,
DetectedEncoding::Utf16Le => writer.write_all(&[0xFF, 0xFE])?,
DetectedEncoding::Utf16Be => writer.write_all(&[0xFE, 0xFF])?,
_ => {}
}
}
// 各行をエンコードして書き込み
for (i, line) in lines.iter() .enumerate() {
let encoded = encode_string(line, encoding)?;
writer.write_all(&encoded)?;
// 最後の行以外は改行を追加(元のファイルと同じ形式)
if i < lines.len() - 1 {
let newline = encode_string("\r\n", encoding)?;
writer.write_all(&newline)?;
}
}
writer.flush()?;
Ok(())
}
fn encode_string(s: &str, encoding: DetectedEncoding) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
match encoding {
DetectedEncoding::Utf8 => Ok(s.as_bytes().to_vec()),
DetectedEncoding::Utf16Le => {
let (encoded, _, had_errors) = UTF_16LE.encode(s);
if had_errors {
eprintln! ("警告: エンコード中にエラーがありました");
}
Ok(encoded.to_vec())
}
DetectedEncoding::Utf16Be => {
let (encoded, _, had_errors) = UTF_16BE.encode(s);
if had_errors {
eprintln! ("警告: エンコード中にエラーがありました");
}
Ok(encoded.to_vec())
}
DetectedEncoding::ShiftJis => {
let (encoded, _, had_errors) = SHIFT_JIS.encode(s);
if had_errors {
eprintln! ("警告: エンコード中にエラーがありました");
}
Ok(encoded.to_vec())
}
}
}
解説:
eprintln! :標準出力ではなく、標準エラーにエラーメッセージを書き込みます。
https://ittrip.xyz/rust/rust-stderr-handling
テキストに含まれる文字の文字毎の出現回数
探偵小説シャーロック・ホームズの「踊る人形」 では、英文では、文字 e の出現回数が最も多いことを利用して暗号を解読しています。
英文だけでなく、日本語も含めたテキスト・ファイルを読んで、そのテキストに含まれる文字の文字毎の出現回数を調べます。
1.ファイルを開くダイアログで、テキスト・ファイルを選びます。
テキスト・ファイルは、日本語と英語です。
文字コードは、Shift JIS ,UTF-8、UTF16 のいずれでも扱えます。
文字コード UTF-16 でファイル保存 を参照下さい。
2.選択したファイルを読み込んで、含まれる文字について、文字毎の出現回数を調べます。
3.含まれる文字とその文字の出現回数を、テキスト・ファイルで出力します。
出力先は、読み込んだファイルと同じフォルダに、読み込んだファイル名の後ろに Count という文字を付けたファイル名とします。
CountOfEachCharacterJP
Cargo.toml
[package]
name = "CountOfEachCharacterJP"
version = "0.1.0"
edition = "2024"
[dependencies]
encoding_rs = "0.8.35"
rfd = "0.17.2"
main.rs
//ChatGPT による
use encoding_rs ::{SHIFT_JIS, UTF_16BE, UTF_16LE, UTF_8};
use std ::collections::HashMap ;
use std::fs ;
use std ::io::Write;
use std ::path::Path;
use std ::io;
/// バイト列から文字コードを判定して UTF-8 String に変換
fn utf8に変換(bytes: &[u8]) -> String {
// UTF-8 BOM
if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
let (cow, _, _) = UTF_8.decode(&bytes[3..]);
return cow.to_string();
}
// UTF-16 LE BOM
if bytes.starts_with(&[0xFF, 0xFE]) {
let (cow, _, _) = UTF_16LE.decode(&bytes[2..]);
return cow.to_string();
}
// UTF-16 BE BOM
if bytes.starts_with(&[0xFE, 0xFF]) {
let (cow, _, _) = UTF_16BE.decode(&bytes[2..]);
return cow.to_string();
}
// ★ BOM 無し UTF-8 を試す
let (cow, _, had_errors) = UTF_8.decode(bytes);
if !had_errors {
// UTF-8 として問題なければ採用
return cow.to_string();
}
// 最後に Shift JIS
let (cow, _, _) = SHIFT_JIS.decode(bytes);
cow.to_string()
}
fn main() {
// @ ファイル選択ダイアログ
let ファイル_パス = rfd ::FileDialog::new()
.add_filter("Text", &["txt"])
.pick_file();
let path = match ファイル_パス {
Some(p) => p,
None => {
println! ("ファイルが選択されませんでした");
return;
}
};
// A ファイル読み込み
let bytes = fs:: read(&path).expect("ファイル読み込み失敗");
// 文字コードを考慮して UTF-8 に変換
let text = utf8に変換(&bytes);
// B 文字ごとの出現回数をカウント(空白・改行除外)
let mut カウント: HashMap <char, usize> = HashMap ::new();
for ch in text.chars() {
if ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' {
continue;
}
*カウント.entry(ch).or_insert(0) += 1 ;
}
// 出力ファイル名作成
let stem = path.file_stem().unwrap().to_string_lossy();
let parent = path.parent().unwrap_or(Path::new("."));
let out_path = parent.join(format!("{}Count.txt", stem));
// C 並び替え
// 1. 出現回数の降順
// 2. 出現回数が同じ場合は文字コード順(昇順)
let mut items: Vec<(char, usize)> = カウント.into_iter().collect();
items.sort_by (|a, b| {
b.1.cmp(&a.1) // 出現回数:多い順
.then_with(|| a.0.cmp(&b.0)) // 文字コード順
});
// C UTF-8 で書き出し
let mut out = fs:: File::create(&out_path).expect("出力ファイル作成失敗");
for (ch, count) in items {
writeln!(out, "{}\t{}", ch, count).unwrap();
}
println! ("文字頻度を出力しました:");
println! ("{:?}", out_path);
// 標準入力を待機
println! ("");
let mut 入力 = String::new();
println! ("Enter で画面を閉じます...");
io::stdin().read_line (&mut 入力).unwrap();
}
解説:
HashMap :
ハッシュマップ型 (HashMap) を扱う
https://maku77.github.io/p/eefwaa3/
HashMapの基本と便利なメソッドの使い方
https://terminart-express.site/archives/3265
ハッシュマップでキーと値のペアを格納する
https://labex.io/ja/tutorials/storing-key-value-pairs-with-rust-hash-maps-100408
キーとそれに紐づいた値をハッシュマップに格納する(The Rust Programming Language 日本語版)
https://doc.rust-jp.rs/book-ja/ch08-03-hash-maps.html
ハッシュマップ(Rust By Example 日本語版)
https://doc.rust-jp.rs/rust-by-example-ja/std/hash.html
RustのHashMapはentryが便利
https://keens.github.io/blog/2020/05/23/rustnohashmaphaentrygabenri/
sort_by
`sort`関数と`sort_unstable`関数の違い
https://ittrip.xyz/rust/rust-collection-sorting-guide#index_id10
Vec型をsort_byで昇順・降順に並び替える
https://dottrail.codemountains.org/rust-vec-sort-by/
構造体Vecをsort_byしやすくするマクロ
https://vraisamis.hatenadiary.jp/entry/2019/08/09/022201
HashMapをソートする方法
https://yiskw713.hatenablog.com/entry/rust-hashmap-sort
RUST ベクトル(構造体)のソート
https://sibainu.lsv.jp/rust-quicksort-revenge-2/