Rust コード例
Rust コード例
このページは Rust のプログラム例を紹介するものです。
このページは、AI に教えてもらったコードを中心に紹介します。
AI (ChatGPT 、Gemini Pro 、Claude 、Qwen(千問) ) のプログラミング言語の知力は優れたものです。
AI は、開発したいプログラムの仕様を伝えると、要求した動作をする日本語コメント付きのコードを、瞬時に返答してくれます。
Rust プログラミング言語の知識やコード記述を知らない私が、日本語で「こんな感じのアプリを作って」といった雰囲気(vibes=vibrations)を AI(LLM=Large Language Models)に伝えて、対話しながら高速でソフトウェアを開発できました。
AI を利用して対話しながらリアルタイムにアプリを開発する手法は、バイブコーディング(Vibe Coding)と呼ばれています。
帰納的学習で、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!
プログラム言語を学ぶ際に、 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!("{}時間", 時間)) 変数を引数に参照渡しするときは変数の前に「& 」を付けます。渡されるのは参照で所有権は元に残ります。
Rustの参照(&)を理解する
format!マクロ
`if 分 > 0 { ... }` もし「分」が0でなければ、文字列に「◯分」を追加します。
`if 秒 > 0 { ... }` もし「秒」が0でなければ、文字列に「◯秒」を追加します。
`if 出力.is_empty() { ... }` もし計算結果が全て0(1秒未満など)で何も文字がない場合、「0秒」と入れます。
`println! ("経過時間: {}", 出力);` 完成した文字列を実行画面(ターミナル)に表示します。
整数型の最小値、最大値を表示
整数型の最小値、最大値を表示します。
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(())
}
解説:
ベクター型 (Vec<T>) は、特定の型 (T) の要素を保持する可変長配列 です。
ベクタの初期化には`vec!`マクロを使用 できます。
ベクタ型
https://doc.rust-lang.org/rust-by-example/ja/std/vec.html
Vec 型の基本について分かりやすく解説
https://rust-tech.nkhn37.net/rust-vec-basic/
ベクター型 (Vec) を扱う
https://maku77.github.io/p/jku3biq/
ベクタで値のリストを保持する
https://doc.rust-jp.rs/book-ja/ch08-01-vectors.html
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
fs::remove_file は、ファイルシステムからファイルを削除します。
https://doc.rust-lang.org/std/fs/fn.remove_file.html
Some(path)
Some(path) の正体:`Some` は関数ではなく **enum のバリアント(種類)** です。
実際はこう定義されています:
enum Option<T> {
Some(T),
None,
}
Some(path) = 「中身がある場合」
None = 「中身がない場合」
だから、Some(path) は、「値があるケースにマッチさせる型パターン」なのです。
Some(T)は何かしらの型を1つ持った、匿名タプル構造体型です。
タプル【tuple】 とは、順序付けられた複数の要素で構成される「組」のことです。
氏名は、名字と名前という2つの文字列の組みで構成されます。
地図の位置は、緯度と経度の組みで構成されます。
後から要素を変更できないリストです。要素を追加・削除・変更できない点が特徴です。
シングル (single)、ダブル (double)、・・・。英語の並びを抽象化して n タプル (n-tuple) と呼ぶようになったことに由来。
RustのSome(T)は、標準ライブラリのOption<T>列挙型における、値が存在することを示すバリアントです。
任意の型Tのデータを1つ持ったタプル構造体の形式をしており、Some(5)やSome("hello")のように、Nullの代わりに安全に値をラップして扱うために使われます。
構造: enumのバリアントで、内部に1つのデータ(T)を持つタプル構造体(Tuple Struct)のような形式(Some(T))をとります。
用途: Rustではnullを直接扱わないため、値の存在(Some)または不在(None)を表現するために、Option<T>列挙型として使います。
多用途: Some(5)(i32型)、Some("hello")(&str型)など、どのような型でも1つ保持できます。
この構造により、コンパイラは「値が必ず存在する」ことを保証し、安全に型チェックを行うことができます。
// 例: Some(T)の使用
let some_number = Some(5);
let some_string = Some("hello");
構造体
https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/0e7a37
タプル構造体
https://rusty.pages.dev/tuple/
rfd の `pick_file()` の戻り値
let file = rfd::FileDialog::new().pick_file();
`pick_file()` の戻り値は:
Option<PathBuf >
になります。つまり:
状況 戻り値
ファイルを選んだ `Some(PathBuf )`
キャンセルした `None`
Rust では「値があるかもしれない」ものを Option で表します。
Rust は、「値が無い可能性」を必ず考えさせる言語なので、Rust は null を禁止している代わりに`Option` を使います。
Option<T>
中身は 2種類だけです:
Some(値) // 値あり
None // 値なし
if let Some(path) = file は、パターンマッチで、次を **1行で書いた便利構文**です。
1. file が Some かチェック
2. 中身を取り出す
3. None なら無視
「file の中身が存在するなら、それを path に取り出す」という意味です。
実際は、取り出しながらチェックしているだけなんです。
展開すると下の意味になります:
match file {
Some(path) => {
// file が Some なら
println!("選択されたファイル: {:?}", path);
// 中身を取り出して path という変数に入れる
}
None => {
// None なら何もしない
}
}
図解すると:
file
├─ Some("C:\test.txt") → path = "C:\test.txt"
└─ None → 実行されない
理解を深めるための例:
let x = Some(10);
if let Some(value) = x {
println!("{}", value);
}
→ 10 が表示される
let x: Option<i32> = None;
if let Some(value) = x {
println!("{}", value);
}
→ 何も起きない
matchアーム記法
match内の各条件分岐(パターン => 式)部分はマッチアームもしくはアームと言います。
パターンマッチング
https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/5adf8a
パターン記法
https://doc.rust-jp.rs/book-ja/ch18-03-pattern-syntax.html
パターンマッチング:実践ガイド
https://note.com/leapcell/n/n1c43c869f9ee
パターン(記法)
https://ytyaru.hatenablog.com/entry/2020/10/22/000000
テキスト・ファイルを行ソート
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 新ファイル名 = if extension.is_empty() {
format! ("{}SortedGemini", stem)
} else {
format! ("{}SortedGemini.{}", stem, extension)
};
path.with_file_name(新ファイル名)
}
テキスト・ファイルを行ソート 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 新ファイル名 = format! (
"{}SortedClaude.{}",
stem.to_string_lossy(),
extension.to_string_lossy()
);
Ok(parent.join(新ファイル名))
}
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/
UTF-8 CSV ファイルを読んで抽出
クジラ飛行机 さんが、Rustで有名アルゴリズムに挑戦 という連載記事を書かれています。
その中に、
12万行のテキストを処理するプログラムを見やすさ・速度・メモリの観点から作ってみよう という記事がありました。
郵便局Webサイトでダウンロードできる郵便番号のCSVファイル「utf_ken_all.csv」を読み込んで抽出するプログラムです。
住所の郵便番号(1レコード1行、UTF-8形式)(CSV形式)
https://www.post.japanpost.jp/zipcode/dl/utf-zip.html
このCSVファイルは、3列目が「郵便番号(7桁)」、5列目が「市区町村名のカナ」、6列目が「町域名のカナ」、7列目が「都道府県名」、8列目が「市区町村名」、9列目が「町域名」となっています。
このプログラムは、
1.CSVファイルを読み込んで、
2.この5列目と6列目を結合して昇順でソートします。
3. 郵便番号(3列目)と住所(8列目と9列目)の最初の5件を表示します。
Gemini に依頼して、CSVファイルの選択はファイルを開くダイアログを使うように、そして、表示した結果の画面を閉じないように変更してもらったコードを、転載させていただきます。
Cargo.toml
[package]
name = "sort_simple_Kujira"
version = "0.1.0"
edition = "2021"
[dependencies]
rfd = "0.15"
main.rs
// sort_simple_Kujira
// くじらはんど
// http://kujirahand.com/
// https://news.mynavi.jp/techplus/article/rustalgorithm-34/
// Rustで有名アルゴリズムに挑戦
// 12万行のテキストを処理するプログラムを見やすさ・速度・メモリの観点から作ってみよう
use std::env; // 実行ファイルの登録フォルダを取得するため
use rfd::FileDialog;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::error::Error;
use std::io;
fn main() -> Result<(), Box<dyn Error>> {
// ファイルダイアログ
// exe(実行ファイル)のパスを取得し、その親ディレクトリ(フォルダ)を取得
let mut dialog = FileDialog::new().add_filter("CSV File", &["csv"]);
// exeの場所が正常に取得できた場合のみ、初期フォルダを設定する
if let Ok(exe_path) = env ::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
dialog = dialog.set_directory(exe_dir); // ★初期フォルダを指定
}
}
// ファイルを開くダイアログ
let 入力パス = dialog
.pick_file()
.ok_or("ファイルが選択されませんでした。")?;
// 対象CSVファイルを開いてBufReaderで読む
//CSVファイルを開いて、BufReaderを使ってデータを読み出します。
//「File::open」はファイルを開くメソッドで、「?」を使うことでエラー処理を簡潔に記述します。
//let file = File::open("utf_ken_all.csv")?;
let file = File::open(入力パス)?;
let reader = BufReader::new(file);
// 住所カナや郵便番号・住所を一時保存する構造体を初期化
let mut entries = Vec ::new();
// 1行ずつ読み込んで CSVフィールドを抽出
for line in reader.lines() {
let line = line?;
if line.is_empty() {
continue;
}
// フィールドを分割してトリムし、必要な情報を抽出
//1行の文字列をカンマ区切りで分割し、CSVの各フィールドを取り出しています。
//CSVでは値が "文字列" のようにダブルクォートで囲まれていることが多いため、
//フィールド前後のダブルクォート「"」を取り除いて、Stringに変換します。
let fields: Vec <String> = line
.split(',')
.map(|s| s.trim_matches('"').to_string())
.collect();
// 住所カナ(5と6列)、郵便番号(3列目)、住所(8と9列目)を取得
let kana_key = format!("{}{}",
fields.get(4).map(String::as_str).unwrap_or(""),
fields.get(5).map(String::as_str).unwrap_or(""));
let postal = fields.get(2).map(String::as_str).unwrap_or("").to_string();
let address = format!("{}{}",
fields.get(7).map(String::as_str).unwrap_or(""),
fields.get(8).map(String::as_str).unwrap_or(""));
// 抽出した情報をタプルとしてベクタに追加
//タプル としてVec 型の entries に追加します。
//タプルは(住所カナ, 郵便番号, 住所)となっています。
entries.push((kana_key, postal, address));
}
// 住所カナをキーに昇順ソートし、先頭5件を出力
//データを住所カナをキーにして昇順ソートして、先頭の5件を表示します。
//ポイントは、Vec 型の entries を、sort_byメソッド でソートしている部分です。
//sort_byの引数にはクロージャ(匿名関数)を指定しています。
//ここでは、Vec 型の entries にはタプルが入っているので、
//タプルの先頭の要素(a.0とb.0)を比較することで、カナ順に並び替えできます。
entries.sort_by (|a, b| a.0.cmp(&b.0));
for (_, postal, address) in entries.iter().take(5) {
println!("{} {}", postal, address);
}
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line(&mut 入力).unwrap();
Ok(())
}
高速化してみたもの:
Cargo.toml
[package]
name = "sort_simple2_Kujira"
version = "0.1.0"
edition = "2021"
[dependencies]
rfd = "0.15"
main.rs
// sort_simple2_Kujira
// くじらはんど
// http://kujirahand.com/
// https://news.mynavi.jp/techplus/article/rustalgorithm-34/
// Rustで有名アルゴリズムに挑戦
// 12万行のテキストを処理するプログラムを見やすさ・速度・メモリの観点から作ってみよう
//高速化してみたもの:
use std::env; // 実行ファイルの登録フォルダを取得するため
use rfd::FileDialog;
use std::fs::File;
use std::io::{self, Read, Write};
use std::error::Error;
// use std::io;
fn main() -> Result<(), Box<dyn Error>> {
// ファイルを丸ごと読み込み、後続でフィールド参照を切り出す
let mut data = Vec ::new();
// ファイルダイアログ
// exe(実行ファイル)のパスを取得し、その親ディレクトリ(フォルダ)を取得
let mut dialog = FileDialog ::new().add_filter("CSV File", &["csv"]);
// exeの場所が正常に取得できた場合のみ、初期フォルダを設定する
if let Ok(exe_path) = env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
dialog = dialog.set_directory(exe_dir); // ★初期フォルダを指定
}
}
// ファイルを開くダイアログ
let 入力パス = dialog
.pick_file()
.ok_or("ファイルが選択されませんでした。")?;
// ファイル全体を読み込む
File::open(入力パス)?.read_to_end(&mut data)?;
if !data.ends_with(b"\n") {
data.push(b'\n');
}
// 行数目安をもとに前取りしてメモリアロケーションを抑制
let estimated_rows = 150_000; // 郵便データ行数目安
let mut entries: Vec <Entry<'_>> = Vec ::with_capacity(estimated_rows);
// バイト列を走査してカンマ位置を探し、必要フィールドだけスライス取得
let mut start = 0;
let mut col = 0;
let mut fields: [&str; 9] = [""; 9];
for i in 0..data.len() {
let b = data[i];
if b == b',' || b == b'\n' {
if col < 9 {
let field_bytes = &data[start..i];
// CSVのダブルクォートは不要との前提なのでトリムのみで対応
let field = trim_quotes(field_bytes);
fields[col] = field;
}
col += 1;
start = i + 1;
if b == b'\n' {
if col >= 9 {
// 住所カナ(5,6列), 郵便番号(3列), 住所(8,9列)
entries.push(Entry {
kana1: fields[4],
kana2: fields[5],
postal: fields[2],
addr1: fields[7],
addr2: fields[8],
});
}
// リセット
col = 0;
fields = [""; 9];
}
}
}
// 住所カナをキーに昇順で上位5件だけを部分選択して出力
let cmp = |a: &Entry, b: &Entry| match a.kana1.cmp(b.kana1) {
std::cmp::Ordering::Equal => a.kana2.cmp(b.kana2),
other => other,
};
let k = entries.len().min(5);
if k > 0 {
// 部分選択で先頭k件の範囲だけを確定させる
entries.select_nth_unstable_by(k - 1, cmp);
// 必要な先頭k件のみを安定・高速にソート
entries[..k].sort_unstable _by(cmp);
let mut stdout = std::io::BufWriter::new(std::io::stdout());
for entry in entries.iter().take(k) {
writeln!(stdout, "{} {}{}", entry.postal, entry.addr1, entry.addr2)?;
}
}
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line(&mut 入力).unwrap();
Ok(())
}
#[derive(Debug, Clone, Copy)]
struct Entry<'a> {
kana1: &'a str,
kana2: &'a str,
postal: &'a str,
addr1: &'a str,
addr2: &'a str,
}
// ダブルクォートを外してUTF-8文字列へ変換
fn trim_quotes(bytes: &[u8]) -> &str {
let mut s = bytes;
if s.starts_with(b"\"") && s.ends_with(b"\"") && s.len() >= 2 {
s = &s[1..s.len() - 1];
}
std::str::from_utf8(s).unwrap_or("")
}
素因数分解
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() :繰返し
Iterator の各種メソッド
https://nossie531.github.io/doc/rust/iterator_methods.xhtml
イテレータ操作まとめ|よく使う16のメソッド
https://zenn.dev/ha_maya0104/articles/6cb18e99c5edbd
「.iter().product()」を使うと、リストの中身を全て掛け算してくれます。`.iter()` でデータを順番に取り出し、`.product()` でそれらを全て掛け合わせます。
「iter().enumerate()」は、コレクション(配列やベクタなど)を反復処理する際に、要素のインデックス(0からの連番)と値の参照(&T)を同時に(ペアで)取得できます。enumerate() は常に 0 から始まるインデックスを生成します。
何番目なのかを得るためには「iter().enumerate()」を使います。
***********************
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/
3.素因数分解(PARI/GP使用)
Rust から PARI/GP (gp.exe) を利用して素因数分解します。
PARI/GP という計算機代数アプリケーショ ンがあり、整数論 (因数分解、 代数的数理論、楕円曲線等) における計算を高速に行なうことができます。
Rust に prime_factorization というライブラリがあるので、先にこれを使った素因数分解のプログラム を実行してみました。
しかし、与える数によっては分解処理に何日も費やしました。
そこで、Gemini に Rust で PARI/GP を利用できないかと聞いたところ、
PARI ライブラリを Rust に直接ビルド・リンクするのは難易度が高く、エラーの温床になりがちです。
GP (gp.exe) をWindows PC にインストールして、Rust からコマンドとして呼び出して利用すると、複雑な設定なしに、PARI/GP の爆速エンジンを利用できます。
と教えてくれました。
GP (gp.exe) は、対話型に動作するコマンドライン・インタフェイスのプログラムです。これを使って PARI の機能を利用できます。
プログラミングでは「**得意なことは、それが得意なツールに任せる**」というアプローチは、非常に強力な設計思想(疎結合)の一つです。
下記のプログラムの構成は、以下のような役割分担が綺麗にできています。
* **Rust (司令塔)**: ユーザーとのやり取り、文字列の整形、表示の制御(得意分野:安全性、処理フローの管理)
* **PARI/GP (計算エンジン)**: 巨大な数の分解、巨大な数の掛け算(得意分野:数値計算、速度)
このプログラムは、与えられた数を、Rust を使って素因数分解した後、その値を掛け合わせて検算チェックしていますが、検算の過程では **「Rust で作った計算式を、もう一度 PARI/GP に投げて計算させる」** という方法で実装しています。これなら新たなライブラリを入れる手間がなく、巨大な桁数でも確実に検算できます。
PARI/GP を呼び出す部分を `call_gp ` という関数にまとめてあります。これにより、「素因数分解のために GP を呼ぶ」「検算のために GP を呼ぶ」という2回の呼び出しがスッキリ記述されています。
Rust 側で `u128` などの数値型 を使わず、すべて「文字列(String)」として扱っているため、**理論上はメモリが許す限り何千桁でも** エラーにならずに処理できるのがこのプログラムの最大の強みです。
右上の画面のように、1日半かかりましたが、100桁の数 RSA-100 = 1522605027922533360535618378132637429718068114961380688657908494580122963258952897654000350692006139 を分解できました!!
(追記:素因数分解(msieve 使用) だと 3時間でした。)
### 1. PARI/GP のダウンロードとインストール
1. **公式サイトへアクセス**: [PARI/GP Download Page]
https://pari.math.u-bordeaux.fr/download.html
2. **Windows版をダウンロード**:
* "Windows, binary distributions" セクションにある Stable 64-bit version **`Pari64-x-y-z.exe`** をダウンロードします。
3. **インストール**:
* ダウンロードした `.exe` を実行します。
* **重要**: インストール後、インストール先の `gp.exe` のフォルダ・パスを Windows 環境変数 Path に追加します。
4. **確認**:
* PowerShell やコマンドプロンプトを開き、`gp --version` と入力してバージョン情報が出ればインストール成功です。
PARI/GP のオプション
コマンドライン・オプションは、短縮形(-f)とPOSIXに似た形(--fast)の両方で利用できます。
数値引数の後に、ユーザーの都合に合わせて修飾子
k(キロ) 、M(メガ) 、G(ギガ) を付けることができます。この場合、引数はそれぞれ 10^3 、10^6、10^9 掛けられます。
-f, --fast
高速スタート(または工場出荷時設定)。起動時に.gprc (gprc.txt)を読み込みません。
-p limit
[非推奨] 起動時に、gpは数論的応用で使用される小さな素数のテーブルを計算します。
primelimit(素数極限) が設定されている場合、テーブルにはデフォルト(= 500000)ではなく、その上限までの素数が含まれます。現在では、この値を変更することはほとんど意味がありません。
-q, --quiet
静粛モード。ヘッダーや履歴番号を表示せず、さよならも言いません。
-D, --default key=val
起動時に、default(key, val) を実行して、
GPRC の設定ファイルの値を上書きします。
「val」は定数値でなければならず、計算を含むことはできません(例:1+1は禁止されています)。
このようなデフォルト設定の文はコマンドラインにいくつでも記述できます。キーは複数回設定でき、最後の設定が優先されます。
-s limit
起動時に割り当てられる gp の内部スタックのサイズ。
gp が領域を使い果たすと、現在の計算を中断し、
the PARI stack overflows ! 例外を発生させます。
これが頻繁に起こる場合は、スタックをより大きくして始めてください。
スタックサイズは、 GP 内で
default(parisize, limit)を使って増やすこともできます。
.gprc(gprc.txt) で parisize を設定すると、セッション間で永続的に利用できます。
デフォルトの 10 倍の 80MB 程度が合理的です。
また、parisizemax を .gprc 内で正の、より大きな値に設定することを推奨します(マシンが耐えられると思われる値、通常は使用可能な RAM の半分程度)。
これにより、計算は parisize の範囲に収めようとしますが、一時的にそれを超えること(最大で parisizemax)を許容します。
小さな スタックでの計算は、データの局所性が向上するため効率的であることに注意してください。
最後に、
threadsize と threadsizemax は、並列版 gp で同様の役割を果たします。
--emacs
gp は Emacs シェルで実行可能です(詳細はGPユーザーマニュアルを参照)。
このフラグは PariEmacs パッケージ (pari.el) とのスムーズなやり取りのために必要です。
pari.el パッケージによって自動的に設定され、Emacs セッション外で設定するとディスプレイの異常が発生します。
--help
利用可能なコマンドラインオプションの概要を表示します。
--test
GP をテストモードで実行します : 履歴番号の表示を抑制し、長い出力行を折り返します(読みやすい差分出力を得るため)。ベンチテスト専用です。
--texmacs
gp は TeXmacs のフロントエンドから実行可能です。
このフラグは TeXmacs によって設定され、特別な目的を使った通信チャネルを有効にします。自分で設定しないでください。
--version
バージョン情報(バナー)を出力して終了します。
--version-short
バージョン番号を出力して終了します。
下記が、PARI/GP (gp.exe) を利用して素因数分解するプログラムです。検算機能付きです!
Cargo.toml
[package]
name = "PrimeFactorizationPARI_GP"
version = "0.1.0"
edition = "2024"
[dependencies]
regex = "1"
main.rs
// PrimeFactorizationPARI_GP
// Gemini に教えてもらいました
// PARI/GP の WEB デモ Try PARI/GP in your browser
// https://pari.math.u-bordeaux.fr/gpwasm.html
//PARI/GP の C言語ライブラリ (`libpari`) を Windows の Rust で直接ビルド・リンクするのは
//難易度が高く、エラーの温床になりがちです。
//そこで、**最も確実で簡単な方法**をご提案します。
//それは、**「Windows に PARI/GP (gp.exe) をインストールし、
//Rust からコマンドとして呼び出す」**という方法です。
//これなら複雑な設定なしに、PARI/GP の爆速エンジンを利用できます。
//このプログラムは、裏で `gp` コマンドを実行し、素因数分解の `factor(数字)` という計算を行わせ、
//その結果、素因数と個数(例: `[3, 2; 5, 1]`)を Rust で解析して表示します。
use std::time; // 時間(Duration, Instant)を使うためのモジュール
//use std::io; // 入力のためのインポート
use std::io::{self, Write}; // 入出力のためのインポート
use std::process::{Command, Stdio}; // Stdio を追加
//* 外部コマンド(gp)とデータをやり取りするためのパイプ機能を使うために必要
// `gp` に計算させるためには、**「標準入力(stdin)」に計算式を流し込む** 必要があります。
use regex ::Regex; // Rust から `gp` コマンドの出力を扱いやすくするために、正規表現を使う
fn main() {
println!("--- PARI/GP 素因数分解 & 検算ツール ---");
// ---------------------------------------
println!("");
println!("素因数分解する数(半素数)の例");
println!("3746238285234848709827");
println!("半素数(semiprime)とは、2つの素数の積で表される自然数");
println!("---------------------------------------");
println!("素因数分解する数の例");
println!("717985416201895737890248682491");
println!("---------------------------------------");
println!("10が18回と1:1010101010101010101010101010101010101");
println!("---------------------------------------");
println!("50桁:12345678901234567890123456789012345678901234567890");
println!("---------------------------------------");
println!("");
// ---------------------------------------
println!("(終了するには 'q' を入力してください)");
// PARI/GP の出力形式 [素数, 指数; 素数, 指数; ...] を解析するための正規表現
// 例: [3, 2; 5, 1] -> (3, 2), (5, 1) "3", "2" と "5", "1"
let re = Regex ::new(r"(\d+),\s*(\d+)").unwrap();
loop {
print!("\n数字を入力(38桁以上も可): ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).expect("読込失敗");
let input_str = input.trim();
if input_str == "q" {
break;
}
// 今の時刻(計測開始地点)を取得
let 開始時刻 = time::Instant::now();
// 数値チェック、入力が数値のみで構成されているか簡易チェック
if input_str.is_empty() || !input_str.chars().all(|c| c.is_ascii_digit()) {
println!("有効な数値を入力してください。");
continue;
}
// ---------------------------------------------
// 1. 素因数分解の実行
// ---------------------------------------------
let factor_cmd = format! ("print(factor({}));", input_str);
match call_gp (&factor_cmd) {
Ok(gp_output) => {
// --- 解析と表示用の文字列に整形ロジック ---
// 解析結果を格納するベクタ [(素数, 指数), ...]
let mut factors : Vec <(String, String)> = Vec ::new();
for cap in re.captures_iter(&gp_output) {
factors .push((cap[1].to_string(), cap[2].to_string()));
}
if factors .is_empty() {
// 素因数が見つからない、または1などの場合
// gp_output 自体を表示して確認できるようにする
println!("結果: {}", gp_output);
continue;
}
// ---------------------------------------------
// 2. 結果の表示 (数学形式に整形)
// ---------------------------------------------
let display_parts: Vec <String> = factors .iter()
.map(|(base, exp)| {
if exp == "1" {
base.clone()
} else {
format! ("{}^{}", base, exp)
}
})
.collect();
let result_string = display_parts.join(" * ");
println!("分解結果: {} = {}", input_str, result_string);
println!("---------------------------------------");
// ---------------------------------------------
// 3. 検算 (Verification)
// ---------------------------------------------
// 検算用の式を作成 (例: "3^2 * 5^1")
// GPに計算させるため、指数が1でも "^1" をつけて明示的に書く形式にします
let calc_parts: Vec <String> = factors .iter()
.map(|(base, exp)| format! ("{}^{}", base, exp))
.collect();
let verify_formula = calc_parts.join(" * ");
// 作成した式を GP に投げて計算させる
let verify_cmd = format! ("print({});", verify_formula);
print!("検算中... ");
io::stdout().flush().unwrap();
match call_gp (&verify_cmd) {
Ok(calc_result) => {
if calc_result == input_str {
println!("OK!");
println!("検証式 : {} = {}", verify_formula, calc_result);
} else {
println!("NG!");
println!("不一致発生: 元の数[{}] != 計算結果[{}]", input_str, calc_result);
}
},
Err(e) => println!("検算エラー: {}", e),
}
},
Err(e) => println!("GP実行エラー: {}", e),
}
// 最初の時刻からの経過時間を取得(Duration型)
let 経過時間 = 開始時刻.elapsed();
// 経過時間の「合計秒数」を取得
let 合計秒数 = 経過時間.as_secs();
// 最終的な結果を表示
println!("経過時間: {} 秒", 合計秒数);
}
// 標準入力を待機
println!("");
let mut 入力 = String::new();
println!("Enter で画面を閉じます...");
io::stdin().read_line(&mut 入力).unwrap();
}
// PARI/GP (gp.exe) を呼び出して結果を文字列で返す関数
// 数値文字列を受け取り、"素因数分解の結果文字列" を返します
// コマンドライン引数でスタックサイズを強制しています
// `gprc.txt` に依存せず、プログラム側で「このくらいのメモリを使ってくれ」と指示します。
// GP には `-s` オプションがあり、これでスタックサイズを指定します。
// RSA-100(公開鍵暗号方式の考案者3人(Rivest、Shamir、Adleman)の頭文字:100桁)
// のような巨大な数を扱う場合、余裕を持って **256MB** 程度割り当てると安心です。
fn call_gp (command_str: &str) -> Result<String, String> {
// 1. gp プロセスを立ち上げる
let mut child = Command::new("gp")
.arg("-q") // quiet mode (バナーを表示しない)
// .arg("-f") // fast mode 【削除】 これがあると gprc.txt を読み込みません!
.arg("-s").arg("256M") // 【追加】 スタックサイズを明示的に 256MB に指定
.stdin(Stdio::piped()) // Rustから入力を送れるようにする
.stdout(Stdio::piped()) // 出力を受け取れるようにする
.spawn()
.map_err(|e| format! ("PARI/GP 起動に失敗: {}", e))?;
{
// 2. gp の標準入力(stdin)に計算コマンドを書き込む
// `gp -q -f` だけを実行する
// 例: "print(factor(11111));\n" を送信
let stdin = child.stdin.as_mut().ok_or("stdinが開けません")?;
stdin.write_all(command_str.as_bytes())
.map_err(|e| format! ("書込失敗: {}", e))?;
// ブロックを抜けると child_stdin が drop され、入力終了が gp に伝わります
}
// 3. gp の処理結果(stdout)を受け取る
let output = child.wait_with_output()
.map_err(|e| format! ("読込に失敗: {}", e))?;
if output.status.success() {
let res = String::from_utf8_lossy(&output.stdout);
Ok(res.trim().to_string())
} else {
let err = String::from_utf8_lossy(&output.stderr);
Err(err.trim().to_string())
}
}
解説:
正規表現 (`Regex`) **:
* PARI/GP は結果を `[2, 3; 3, 1; 5, 1]` のような行列形式で返します。
これを Rust 側で「左の数字(素数)」と「右の数字(指数)」として読み取るために正規表現を使用しました。
検算ロジック部分
* `factors ` ベクタに入っている `(素数, 指数)` の情報を使って、Rust 側で掛け算の式(文字列)を組み立てます(例: `"3^2 * 5^1"`)。
* その式を `call_gp ` に渡し、PARI/GP に計算させます。
* 帰ってきた答えが、最初に入力した数字と文字列として完全に一致するか判定しています。
4.素因数分解(ファイル入力でPARI/GP使用)
PARI/GP (gp.exe) を利用すると、大きな素数を掛け合わせた桁数の多い数でも、爆速で素因数分解できる ことが分かりました。
そこで、テキスト・ファイルでデータを与えて、テキスト・ファイルで結果を受け取れるプログラムを Gemini に教えてもらいました。
仕様:
1.素因数分解したい数値を何行か書いたテキスト・ファイルを、ファイルを開くダイアログで指定して読み込みます。
2.このファイルの数値を、PARI/GP の素因数分解を利用して実行します。
3.素因数分解結果は、数値を読んだファイル名の後ろに「_PrimeFactorization」を付けたファイル名で、読み込んだフォルダと同じフォルダに書き込みます。
Cargo.toml
[package]
name = "gp_factorizer_file"
version = "0.1.0"
edition = "2021"
[dependencies]
regex = "1"
rfd = "0.12" # ファイルダイアログ用のライブラリ
main.rs
// gp_factorizer_file
// Gemini に教えてもらいました
//PARI/GP の C言語ライブラリ (`libpari`) を Windows の Rust で直接ビルド・リンクするのは
//難易度が高く、エラーの温床になりがちです。
//そこで、**最も確実で簡単な方法**をご提案します。
//それは、**「Windows に PARI/GP (gp.exe) をインストールし、
//Rust からコマンドとして呼び出す」**という方法です。
//これなら複雑な設定なしに、PARI/GP の爆速エンジンを利用できます。
//このプログラムは、裏で `gp` コマンドを実行し、素因数分解の `factor(数字)` という計算を行わせ、
//その結果、素因数と個数(例: `[3, 2; 5, 1]`)を Rust で解析して表示します。
//1.分解したい数値を何行か書いたテキスト・ファイルを、ファイルを開くダイアログで指定して読み込みます。
//2.PARI/GP を利用して素因数分解します。
//3.結果を、読んだファイル名の後ろに「_PrimeFactorization」を付けたファイル名で、
//// 読み込んだフォルダと同じフォルダに書き込みます。
use regex::Regex;
use rfd::FileDialog;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time; // 時間(Duration, Instant)を使うためのモジュール
fn main() {
println!("--- PARI/GP ファイル一括素因数分解ツール ---");
// 1. ファイルを開くダイアログを表示
println!("読み込むテキストファイルを選択してください...");
let file_path = match FileDialog ::new()
.add_filter("Text", &["txt"])
.set_directory(".") // カレントディレクトリから開始
.pick_file()
{
Some (path) => path,
None => {
println!("ファイルが選択されませんでした。終了します。");
return;
}
};
println!("選択されたファイル: {:?}", file_path);
// 今の時刻(計測開始地点)を取得
let 開始時刻 = time::Instant::now();
// 2. 出力ファイル名の作成 (元のファイル名_PrimeFactorization.txt)
let output_path = {
let parent = file_path.parent().unwrap_or(Path::new("."));
let file_stem = file_path.file_stem().unwrap().to_string_lossy();
let 新ファイル名 = format! ("{}_PrimeFactorization.txt", file_stem);
parent.join(新ファイル名)
};
// ファイルのオープン(読み込み用と書き込み用)
let input_file = File::open(&file_path).expect("入力ファイルが開けません");
let reader = BufReader::new(input_file);
let mut output_file = File::create(&output_path).expect("出力ファイルが作成できません");
// 出力解析用の正規表現 (事前にコンパイル)
let re = Regex::new(r"(\d+),\s*(\d+)").unwrap();
println!("処理を開始します...");
let mut count = 0;
// 3. 行ごとの処理
for line in reader.lines() {
let line_content = line.unwrap_or_default();
let input_num = line_content.trim();
// 空行や数値以外はスキップ(そのまま出力するか、無視するか選べます)
if input_num.is_empty() || !input_num.chars().all(|c| c.is_ascii_digit()) {
continue;
}
print!("分解中: {} ... ", input_num);
io::stdout().flush().unwrap();
// GPを実行して結果文字列を取得
match run_gp_factorization(input_num, &re) {
Ok(result_str) => {
// 結果をファイルに書き込む
writeln!(output_file, "{} = {}", input_num, result_str).unwrap();
println!("完了");
}
Err(e) => {
println!("エラー: {}", e);
writeln!(output_file, "{} = エラー: {}", input_num, e).unwrap();
}
}
count += 1;
}
println!("--------------------------------------------------");
println!("すべての処理が完了しました。");
println!("処理件数: {} 件", count);
println!("出力ファイル: {:?}", output_path);
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!("\nEnterキーを押して終了してください...");
let mut temp = String::new();
io::stdin().read_line(&mut temp).ok();
}
// 素因数分解部分のロジックを関数化しました
// 数値文字列を受け取り、"素因数分解の結果文字列" を返します
// コマンドライン引数でスタックサイズを強制しています
// `gprc.txt` に依存せず、プログラム側で「このくらいのメモリを使ってくれ」と指示します。
// GP には `-s` オプションがあり、これでスタックサイズを指定します。
// RSA-100(公開鍵暗号方式の考案者3人(Rivest、Shamir、Adleman)の頭文字:100桁)
// のような巨大な数を扱う場合、余裕を持って **256MB** 程度割り当てると安心です。
fn run_gp_factorization(target_number: &str, re: &Regex) -> Result<String, String> {
// 1. gp プロセスを立ち上げる
let mut child = Command::new("gp")
.arg("-q") // quiet mode (バナーを表示しない)
// .arg("-f") // fast mode 【削除】 これがあると gprc.txt を読み込みません!
.arg("-s").arg("256M") // 【追加】 スタックサイズを明示的に256MBに指定
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.map_err(|e| format! ("起動失敗: {}", e))?;
// 2. gp にコマンド送信
{
let child_stdin = child.stdin.as_mut().ok_or("stdin取得失敗")?;
let command = format! ("print(factor({}));\n", target_number);
child_stdin
.write_all(command.as_bytes())
.map_err(|e| format! ("送信エラー: {}", e))?;
}
// 3. 結果受信
let output = child.wait_with_output().map_err(|e| format! ("読込失敗: {}", e))?;
if !output.status.success() {
let err_msg = String::from_utf8_lossy(&output.stderr);
return Err(format! ("GP計算エラー: {}", err_msg));
}
let gp_output = String::from_utf8_lossy(&output.stdout);
// --- 解析ロジック ---
let mut result_parts = Vec ::new();
for cap in re.captures_iter(&gp_output) {
let base = &cap[1];
let exp = &cap[2];
if exp == "1" {
result_parts.push(base.to_string());
} else {
result_parts.push(format! ("{}^{}", base, exp));
}
}
if result_parts.is_empty() {
// 結果が空の場合(通常ありえませんが念のため生の結果を返す)
Ok(gp_output.trim().to_string())
} else {
Ok(result_parts.join(" * "))
}
}
解説:
1. **ダイアログ機能 (`rfd::FileDialog `)**:
* `rfd` (Rust File Dialog) クレートを使うと、Windows標準の「ファイルを開く」画面を簡単に呼び出せます。
* `.add_filter("Text", &["txt"])` でテキストファイルだけを選べるようにしています。
2. **ファイル名の生成**:
* 読み込んだファイルのパス情報 (`file_path`) から、フォルダパス (`parent`) と拡張子抜きのファイル名 (`file_stem`) を取り出し、新しいファイル名 (`_PrimeFactorization.txt`) を組み立てています。
* これにより、読み込んだファイルと同じ場所に結果ファイルが保存されます。
3. **読み書きのループ**:
* `reader.lines()` で入力ファイルを1行ずつ読み込みます。
* `run_gp_factorization` という関数を作り、以前の `main` にあった PARI/GP を呼び出す処理を閉じ込めました。これでメインの流れがスッキリしました。
* 計算結果は画面に「完了」と表示しつつ、ファイルにも書き込んで (`writeln!`) いきます。
5.素因数分解(msieve 使用)
素因数分解プログラムといえば、msieve が著名です。
https://sourceforge.net/projects/msieve/files/msieve/Msieve%20v1.53/
Msieve の使い方
https://stdkmd.net/nrr/msieve_ja.htm
3.1. 主なオプション
https://stdkmd.net/nrr/msieve_ja.htm#usage_options
素因数分解プログラム msieve
https://sekika.github.io/2015/11/08/msieve/
ダウンロードした msieve153_win64.zip は、インストール不要で、適当なフォルダに解凍すれば使えるようになります。
コマンド・プロンプトから使うためには、環境変数を編集で、解凍したフォルダを Path 登録する必要があります。
次の仕様を Claude に伝えたら、下記のプログラムを即答してくれました。
素因数分解(ファイル入力で msieve 使用) もあります。
1.表示した画面で、素因数分解したい数の入力を求めます。
2.入力した数値に対して、算術表現で素因数分解結果を表示します。
3.その後、素因数分解結果を掛け合わせて、元の数と合致することを検証します。
4.検証した結果、元の数値と合致していた場合は、検証結果:OK と表示します。
5.次の数の入力を求めます。もし q と入力したらプログラムを終了します。
なお、実行ファイルと同じフォルダに作られる中間ファイル msieve.dat を、処理後に削除します。
右上の画面のように、100桁の数 RSA-100 = 1522605027922533360535618378132637429718068114961380688657908494580122963258952897654000350692006139 を 3時間 で素因数分解できました!!
Cargo.toml
[package]
name = "msieve_factorizer"
version = "0.1.0"
edition = "2021"
[dependencies]
num-bigint = "0.4"
num-traits = "0.2"
main.rs
// msieve_factorizer
// Claude に教えてもらいました。
use num_bigint::BigUint; //ヒープ上で任意桁の整数を保持するため、
//理論上は桁数の上限がありません(実際には msieve 自身の上限に依存します)
use num_traits::One;
use std::io::{self, Write};
use std::process::Command;
use std::str::FromStr;
use std::time; // 時間(Duration, Instant)を使うためのモジュール
use std::fs::{self};
use std::path::{Path}; //ファイルやディレクトリのパス情報を扱えるように
// ───────────────────────────────────
// main
// ───────────────────────────────────
fn main() {
println!("============================================================");
println!(" msieve 素因数分解ツール (Rust版) ※100桁以上対応");
println!("============================================================");
println!("終了するには 'q' を入力してください。");
println!();
loop {
print!("★ 素因数分解する数を入力してください(q で終了): ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
// 終了
if input.eq_ignore_ascii_case("q") {
println!("プログラムを終了します。");
break;
}
// 空入力
if input.is_empty() {
println!("数値を入力してください。\n");
continue;
}
// バリデーション
let n = match parse_input (input) {
Ok(v) => v,
Err(msg) => {
println!("{} ('q' で終了)\n", msg);
continue;
}
};
// 今の時刻(計測開始地点)を取得
let 開始時刻 = time::Instant::now();
// 桁数表示(大きな数には桁数も表示)
let digits = input.len();
if digits > 15 {
println!(
"({} 桁の数) msieve で素因数分解中... しばらくお待ちください。",
digits
);
} else {
println!("msieve で素因数分解中... しばらくお待ちください。");
}
match 素因数分解 (input) {
Ok(factors) => {
// 分解結果
let short_expr = format_factors_short (&n, &factors);
println!("分解結果: {}", short_expr);
println!("");
// 検証式
let full_expr = format_factors_full (&n, &factors);
println!("検証式 : {}", full_expr);
// 検証
if verify_factors (&n, &factors) {
println!("検証結果: OK ○");
} else {
println!("検証結果: NG × (積が元の数と一致しません)");
}
}
Err(e) => {
eprintln!("エラー: {}", e);
}
}
// 最初の時刻からの経過時間を取得(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!();
// msieve.dat を毎回削除(次の処理のため)
delete_msieve_dat ();
}
}
// ───────────────────────────────────
// msieve を呼び出して素因数分解
// msieve153.exe をサブプロセスで呼び出し、stdout/stderr を取得
// ───────────────────────────────────
fn 素因数分解 (n_str: &str) -> Result<Vec <(BigUint, u32)>, String> {
let output = Command::new("msieve153.exe")
.args(["-e", "-v", n_str])
// -e:素因数分解に楕円曲線法(ECM)を用いる。-v:結果(ログ)を画面に表示する
.output()
.map_err(|e| format!("msieve153.exe の実行に失敗しました: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
// msieve は stdout / stderr の両方に情報を出す場合がある
let combined = format!("{}\n{}", stdout, stderr);
// エラー文字列チェック("error" を含む行のみ確認)
let has_error = combined
.lines()
.any(|l| l.to_lowercase().contains("error"));
if has_error {
return Err(format!("msieve エラー:\n{}", combined));
}
let factors = parse_factors (&combined);
if factors.is_empty() {
Err(format!(
"素因数を取得できませんでした。\nmsieve 出力:\n{}",
combined
))
} else {
Ok(factors)
}
}
// ───────────────────────────────────
// msieve 出力のパース
// msieve は素因数を以下の形式で出力します:
// p1: 3
// p3: 101
// p10: 1234567891 ← 桁数が大きい場合も BigUint で受け取る
// p3: 101 形式の行を正規表現なしでパースし (素因数, 指数) に整理
// ───────────────────────────────────
fn parse_factors (output: &str) -> Vec <(BigUint, u32)> {
let mut factors: Vec <BigUint> = Vec ::new();
for line in output.lines() {
let trimmed = line.trim();
// "p" で始まり ":" を含む行が素因数行
if trimmed.starts_with('p') && trimmed.contains(':') {
let parts: Vec <&str> = trimmed.splitn(2, ':').collect();
if parts.len() == 2 {
let num_str = parts[1].trim();
if let Ok(n) = BigUint::from_str(num_str) {
if n > BigUint::one() {
factors.push(n);
}
}
}
}
}
// 昇順ソート後、同じ素因数をまとめて (素因数, 指数) のペアにする
factors.sort();
let mut result: Vec <(BigUint, u32)> = Vec ::new();
for f in factors {
if let Some (last) = result.last_mut() {
if last.0 == f {
last.1 += 1;
continue;
}
}
result.push((f, 1));
}
result
}
// ───────────────────────────────────
// 表示用フォーマット
// ───────────────────────────────────
/// 分解結果行: 指数1は省略
/// 例) 1234567890123456789 = 3^2 * 101 * 3541 * …
/// 指数1は省略した 分解結果 用の文字列を生成
/// &BigUint, ... 参照渡しでコピーコスト削減
fn format_factors_short (n: &BigUint, factors: &[(BigUint, u32)]) -> String {
let parts: Vec <String> = factors
.iter()
.map(|(base, exp)| {
if *exp == 1 {
format!("{}", base)
} else {
format!("{}^{}", base, exp)
}
})
.collect();
format!("{} = {}", n, parts.join(" * "))
}
/// 検証式行: 全ての指数を表示
/// 例) 3^2 * 101^1 * … = 1234567890123456789
/// 全指数を表示した 検証式 用の文字列を生成
fn format_factors_full (n: &BigUint, factors: &[(BigUint, u32)]) -> String {
let parts: Vec <String> = factors
.iter()
.map(|(base, exp)| format!("{}^{}", base, exp))
.collect();
format!("{} = {}", parts.join(" * "), n)
}
// ───────────────────────────────────
// 検証: 素因数の積 == 元の数
// ───────────────────────────────────
fn verify_factors (n: &BigUint, factors: &[(BigUint, u32)]) -> bool {
let product = factors
.iter()
.fold(BigUint::one(), |acc, (base, exp)| acc * base.pow(*exp));
&product == n
}
// ───────────────────────────────────
// 入力文字列のバリデーション
// 入力文字列が正の整数 (>= 2) かどうか確認
// ───────────────────────────────────
fn parse_input (s: &str) -> Result<BigUint, &'static str> {
// 符号付き入力は受け付けない(msieve に渡す文字列をそのまま使うため)
if s.starts_with('+') || s.starts_with('-') {
return Err("符号なしの正の整数を入力してください。");
}
match BigUint::from_str(s) {
Ok(n) if n >= BigUint::from(2u32) => Ok(n),
Ok(_) => Err("2以上の整数を入力してください。"),
Err(_) => Err("有効な整数を入力してください。"),
}
}
// ────────────────────
// msieve.dat 削除
// msieve.dat は、処理過程を記録する中間ファイルで、大きなサイズです。
// msieve.dat は、直前に中断・完了した分解を再開できるようにするためのものです。
// ────────────────────
fn delete_msieve_dat () {
let dat_path = Path::new("msieve.dat");
if dat_path.exists() {
if let Err(e) = fs::remove_file (dat_path) {
eprintln!("警告: msieve.dat の削除に失敗しました: {}", e);
} else {
println!("msieve.dat を削除しました。");
}
println!();
}
}
6.素因数分解(ファイル入力で msieve 使用)
素因数分解(msieve 使用) で、高速処理できることが分かったので、対象数値をファイルから読み込む下記の仕様を Claude に伝えました。
1.分解したい数値を何行か書いたテキスト・ファイルを、ファイルを開くダイアログで指定して読み込みます。
サンプル・ファイルをダウンロードできます→sample_input.zip
(日本語コメントを付ける場合には、ファイルの文字コードを UTF-8 にして下さい。)
2.素因数分解(msieve 使用) のコードを、ファイル用に修正したもので実行します。
3.結果を、読んだファイル名の後ろに「_factor」を付けたファイル名で、読み込んだフォルダと同じフォルダに書き込みます。
下記が Claude から回答を得たコードです。
Cargo.toml
[package]
name = "msieve_factor_File"
version = "0.1.0"
edition = "2021"
[dependencies]
num-bigint = "0.4"
num-traits = "0.2"
rfd = "0.15"
main.rs
// msieve_factor_File
// Claude に教えてもらいました。
use num_bigint::BigUint;
use num_traits::One;
use rfd::FileDialog;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf }; //ファイルやディレクトリのパス情報を扱えるように
//パスとファイルシステムの操作
use std::process::Command;
use std::str::FromStr;
use std::time; // 時間(Duration, Instant)を使うためのモジュール
// ────────────────────
// main
// ────────────────────
fn main() {
println!("============================================================");
println!(" msieve バッチ素因数分解ツール (Rust版)");
println!("============================================================");
println!("素因数分解する数値が記載されたテキストファイルを選択してください。");
println!();
// ファイルダイアログ
let ファイルパス = FileDialog::new()
.add_filter("テキストファイル", &["txt"])
.add_filter("すべてのファイル", &["*"])
.set_title("素因数分解する数値を含むファイルを選択")
.pick_file();
let 入力パス = match ファイルパス {
Some (path) => path,
// この`path`は**変数名**です。
// `FileDialog`が返す`Option<PathBuf >`型の値を分解して取り出したものです。
// `Some (path)`でマッチすると、`path`には`PathBuf `型の値が入ります
// `PathBuf `は**所有権を持つパス**(ヒープに確保された、変更可能なパス情報)
None => {
println!("ファイルが選択されませんでした。プログラムを終了します。");
return;
}
};
// 今の時刻(計測開始地点)を取得
let 開始時刻 = time::Instant::now();
// バッチ処理実行
match process_batch_file (&入力パス) {
Ok(_) => {
println!("\nすべての処理が正常に完了しました。");
}
Err(e) => {
eprintln!("\nエラーが発生しました: {}", e);
}
}
// 最初の時刻からの経過時間を取得(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!("\nEnterキーを押して終了してください...");
let mut dummy = String::new();
std::io::stdin().read_line(&mut dummy).ok();
}
// ────────────────────
// バッチ処理: ファイルから読み込んで素因数分解して結果をファイルに書き込む
// ────────────────────
fn process_batch_file (入力パス: &Path ) -> Result<(), String> {
// 入力ファイルを開く
// この`&Path`は**型名**です。具体的には:
// `Path`は**パスを表す型**(標準ライブラリの`std::path::Path`)
// `&Path`は**Pathへの参照**(借用)
let file = File::open(入力パス)
.map_err(|e| format!("ファイル '{}' を開けませんでした: {}", 入力パス.display(), e))?;
let reader = BufReader::new(file);
// 出力ファイル名を生成
let 出力パス = 出力パス生成 (入力パス);
let mut 出力ファイル = File::create(&出力パス)
.map_err(|e| format!("出力ファイル '{}' を作成できませんでした: {}", 出力パス.display(), e))?;
println!("入力ファイル: {}", 入力パス.display());
println!("出力ファイル: {}", 出力パス.display());
println!();
// ヘッダー書き込み
writeln!(出力ファイル, "============================================================")
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, " msieve 素因数分解 バッチ処理結果")
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "============================================================")
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "入力ファイル: {}", 入力パス.display())
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "")
.map_err(|e| format!("書き込みエラー: {}", e))?;
let mut line_number = 0;
let mut success_count = 0;
let mut error_count = 0;
// 各行を処理
for line_result in reader.lines() {
line_number += 1;
let line = line_result
.map_err(|e| format!("行{}の読み込みエラー: {}", line_number, e))?;
let input = line.trim();
// 空行やコメント行をスキップ
if input.is_empty() || input.starts_with('#') || input.starts_with("//") {
continue;
}
println!("───────────────────────────");
println!("[行 {}] 処理中: {}", line_number, input);
writeln!(出力ファイル, "───────────────────────────")
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "[行 {}] 入力: {}", line_number, input)
.map_err(|e| format!("書き込みエラー: {}", e))?;
// バリデーション
let n = match parse_input (input) {
Ok(v) => v,
Err(msg) => {
println!("エラー: {}", msg);
writeln!(出力ファイル, "エラー: {}", msg)
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "")
.map_err(|e| format!("書き込みエラー: {}", e))?;
error_count += 1;
continue;
}
};
// 桁数表示
let digits = input.len();
println!("({} 桁) msieve で素因数分解中...", digits);
// 素因数分解実行
match 素因数分解 (input) {
Ok(factors) => {
let short_expr = format_factors_short (&n, &factors);
let full_expr = format_factors_full (&n, &factors);
let verification_ok = verify_factors (&n, &factors);
println!("分解結果: {}", short_expr);
println!("検証式 : {}", full_expr);
if verification_ok {
println!("検証結果: OK 〇");
} else {
println!("検証結果: NG ×");
}
writeln!(出力ファイル, "分解結果: {}", short_expr)
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "検証式 : {}", full_expr)
.map_err(|e| format!("書き込みエラー: {}", e))?;
if verification_ok {
writeln!(出力ファイル, "検証結果: OK ?")
.map_err(|e| format!("書き込みエラー: {}", e))?;
} else {
writeln!(出力ファイル, "検証結果: NG ?")
.map_err(|e| format!("書き込みエラー: {}", e))?;
}
writeln!(出力ファイル, "")
.map_err(|e| format!("書き込みエラー: {}", e))?;
success_count += 1;
// msieve.dat を毎回削除(次の処理のため)
delete_msieve_dat ();
}
Err(e) => {
println!("エラー: {}", e);
writeln!(出力ファイル, "エラー: {}", e)
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "")
.map_err(|e| format!("書き込みエラー: {}", e))?;
error_count += 1;
// エラー時も msieve.dat を削除
delete_msieve_dat ();
}
}
println!();
}
// サマリー
println!("============================================================");
println!(" 処理完了");
println!("============================================================");
println!("成功: {} 件", success_count);
println!("失敗: {} 件", error_count);
println!("出力ファイル: {}", 出力パス.display());
writeln!(出力ファイル, "============================================================")
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, " 処理完了")
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "============================================================")
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "成功: {} 件", success_count)
.map_err(|e| format!("書き込みエラー: {}", e))?;
writeln!(出力ファイル, "失敗: {} 件", error_count)
.map_err(|e| format!("書き込みエラー: {}", e))?;
Ok(())
}
// ────────────────────
// msieve を呼び出して素因数分解
// msieve153.exe をサブプロセスで呼び出し、stdout/stderr を取得
// ────────────────────
fn 素因数分解(n_str: &str) -> Result<Vec <(BigUint, u32)>, String> {
let output = Command::new("msieve153.exe")
.args(["-e", "-v", n_str])
// -e:素因数分解に楕円曲線法(ECM)を用いる。-v:結果(ログ)を画面に表示する
.output()
.map_err(|e| format!("msieve153.exe の実行に失敗しました: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
// msieve は stdout / stderr の両方に情報を出す場合がある
let combined = format!("{}\n{}", stdout, stderr);
// エラー文字列チェック("error" を含む行のみ確認)
let has_error = combined
.lines()
.any(|l| l.to_lowercase().contains("error"));
if has_error {
return Err(format!("msieve エラー:\n{}", combined));
}
let factors = parse_factors(&combined);
if factors.is_empty() {
Err(format!(
"素因数を取得できませんでした。\nmsieve 出力:\n{}",
combined
))
} else {
Ok(factors)
}
}
// ────────────────────
// msieve 出力のパース
// msieve は素因数を以下の形式で出力します:
// p1: 3
// p3: 101
// p10: 1234567891 ← 桁数が大きい場合も BigUint で受け取る
// p3: 101 形式の行を正規表現なしでパースし (素因数, 指数) に整理
// ────────────────────
fn parse_factors(output: &str) -> Vec <(BigUint, u32)> {
let mut factors: Vec <BigUint> = Vec ::new();
for line in output.lines() {
let trimmed = line.trim();
// "p" で始まり ":" を含む行が素因数行
if trimmed.starts_with('p') && trimmed.contains(':') {
let parts: Vec <&str> = trimmed.splitn(2, ':').collect();
if parts.len() == 2 {
let num_str = parts[1].trim();
if let Ok(n) = BigUint::from_str(num_str) {
if n > BigUint::one() {
factors.push(n);
}
}
}
}
}
// 昇順ソート後、同じ素因数をまとめて (素因数, 指数) のペアにする
factors.sort();
let mut result: Vec <(BigUint, u32)> = Vec ::new();
for f in factors {
if let Some (last) = result.last_mut() {
if last.0 == f {
last.1 += 1;
continue;
}
}
result.push((f, 1));
}
result
}
// ────────────────────
// 表示用フォーマット
// ────────────────────
/// 分解結果行: 指数1は省略
/// 例) 1234567890123456789 = 3^2 * 101 * 3541 * …
/// 指数1は省略した 分解結果 用の文字列を生成
/// &BigUint, ... 参照渡しでコピーコスト削減
fn format_factors_short(n: &BigUint, factors: &[(BigUint, u32)]) -> String {
let parts: Vec <String> = factors
.iter()
.map(|(base, exp)| {
if *exp == 1 {
format!("{}", base)
} else {
format!("{}^{}", base, exp)
}
})
.collect();
format!("{} = {}", n, parts.join(" * "))
}
/// 検証式行: 全ての指数を表示
/// 例) 3^2 * 101^1 * … = 1234567890123456789
/// 全指数を表示した 検証式 用の文字列を生成
fn format_factors_full(n: &BigUint, factors: &[(BigUint, u32)]) -> String {
let parts: Vec <String> = factors
.iter()
.map(|(base, exp)| format!("{}^{}", base, exp))
.collect();
format!("{} = {}", parts.join(" * "), n)
}
// ────────────────────
// 検証: 素因数の積 == 元の数
// ────────────────────
fn verify_factors(n: &BigUint, factors: &[(BigUint, u32)]) -> bool {
let product = factors
.iter()
.fold(BigUint::one(), |acc, (base, exp)| acc * base.pow(*exp));
&product == n
}
// ────────────────────
// 入力文字列のバリデーション
// 入力文字列が正の整数 (>= 2) かどうか確認
// ────────────────────
fn parse_input(s: &str) -> Result<BigUint, String> {
// 符号付き入力は受け付けない(msieve に渡す文字列をそのまま使うため)
if s.starts_with('+') || s.starts_with('-') {
return Err("符号なしの正の整数を入力してください。".to_string());
}
match BigUint::from_str(s) {
Ok(n) if n >= BigUint::from(2u32) => Ok(n),
Ok(_) => Err("2以上の整数を入力してください。".to_string()),
Err(_) => Err("有効な整数を入力してください。".to_string()),
}
}
// ────────────────────
// 出力ファイル名の生成: input.txt → input_factor.txt
// ────────────────────
fn 出力パス生成 (入力パス: &Path ) -> PathBuf {
let parent = 入力パス.parent().unwrap_or(Path::new("."));
let stem = 入力パス.file_stem().unwrap_or_default();
let ext = 入力パス.extension().unwrap_or_default();
let 新ファイルパス = if ext.is_empty() {
format!("{}_factor", stem.to_string_lossy())
} else {
format!("{}_{}.{}", stem.to_string_lossy(), "factor", ext.to_string_lossy())
};
parent.join(新ファイルパス)
}
// ────────────────────
// msieve.dat 削除
// msieve.dat は、処理過程を記録する中間ファイルで、大きなサイズです。
// msieve.dat は、直前に中断・完了した分解を再開できるようにするためのものです。
// ────────────────────
fn delete_msieve_dat() {
let dat_path = Path::new("msieve.dat");
if dat_path.exists() {
if let Err(e) = fs::remove_file(dat_path) {
eprintln!("警告: msieve.dat の削除に失敗しました: {}", e);
} else {
println!("msieve.dat を削除しました。");
}
}
}
解説:
## PathBuf と &Path の関係
Rustには 2つのパス型があります:
型 説明 例え
`PathBuf` 所有権を持つパス(変更可能) `String`のようなもの
`&Path` パスへの参照(読み取り専用) `&str`のようなもの
### 上のコードを選択したメリット:
1. **所有権の移動を避ける** - `&Path`で借用するので、関数内でパスを使った後も`main`で`input_path`を使える
2. **メモリ効率** - パス情報をコピーせず、参照だけを渡す
3. **柔軟性** - `PathBuf`でも`&Path`でも受け取れる関数設計
// PathBuf → &Path への変換は自動的に行われる
let owned: PathBuf = PathBuf::from("C:\\Users\\file.txt");
let borrowed: &Path = &owned; // 参照を取る
// 関数に渡すとき
process_batch_file(&owned); // PathBuf の参照を渡す
let input_path = match file_path {
Some (path) => path, // PathBuf を取得(所有権を移動)
None => { return; }
};
// PathBuf を所有しているが、関数には参照で渡す
process_batch_file(&input_path); // &PathBuf は自動的に &Path になる
// ^^^^^^^^^^^^^ 借用記号 & で参照を渡す
Option型に有効な値を束縛するときは Some を使います。
https://doc.rust-lang.org/rust-by-example/ja/std/option.html
## より詳しい例
fn main() {
// PathBuf を取得(所有権あり)
let input_path: PathBuf = PathBuf::from("input.txt");
// 関数に参照で渡す
process_batch_file(&input_path); // 所有権は main に残る
// まだ input_path を使える!
println!("処理したファイル: {}", input_path.display());
}
fn process_batch_file(input_path: &Path) -> Result<(), String> {
// ↑ &Path で受け取る = 借用する = 読み取り専用
// パスの情報を読むだけ(変更はしない)
println!("ファイル名: {:?}", input_path.file_name());
println!("拡張子: {:?}", input_path.extension());
Ok(())
}
コード 意味
`path` 変数名(中身は`PathBuf`型の値)
`&Path` 型名(Pathへの参照型)
`&input_path` `input_path`の参照を取る操作
&str (文字列スライス)
特徴: メモリ上の固定された位置にあるUTF-8バイト配列への不変な参照。
用途: 文字列リテラル("Hello")など、事前に値がわかっており、プログラム実行中に変更する必要がない場合。
String (String型)
特徴: ヒープ領域に格納され、実行時にサイズを変更できる、所有権を持つ文字列。
用途: ユーザー入力、文字列の連結、削除、変更など、動的な操作が必要な場合。
配列がベクターになることで「長さが自由に変えられる」ようになるのと同じように、strがStringになることで「文字列の中身を自由に変更・拡張できる」ようになります。
String 型と文字列スライス &str 型
https://rust-tech.nkhn37.net/rust-string-str-basic/
スライスというのは固定配列の一部分で自分を表現する型の事で、元となる配列(サイズ変更が出来ないコンパイル時にサイズが確定する文字配列)が必要になります。
つまり&str型は「元となる固定配列を参照する事で存在出来る型」という事です。そのため&(参照)が付いています。
可変長な「String」と固定長な「&str」
https://note.com/marupeke296/n/n9b69cc5b45d4
Rust 用語集
https://panda-program.com/posts/book-understanding-rust
所有権
既存の変数を新しい変数と = で結びつけたときに、新たなメモリ領域が確保され、既存の変数の値を新たなメモリ領域にコピーする方式を「コピーセマンティクス」と呼んでいます。
既存の変数を新しい変数に = で結びつけたときに、新たなメモリ領域を確保せず、所有権を移動させて新しい変数のラベルに付け替える方式を「ムーブセマンティクス」と呼んでいます。
所有権という考え方を導入すると、メモリ上に格納された値とラベルである変数が1対1に対応します。
プリミティブ型には、Copyトレイトが実装されていて、ほかの変数に束縛しても所有権を失わないことが共通した特徴です。
借用
値の代わりにリファレンスを関数の引数に渡しても、所有権が関数内の変数に移動しない
リファレンスのスコープは参照する元の変数のスコープよりも小さい必要があります。これを所有権の言葉で言い換えれば、「リファレンスは参照する元の変数の所有権のライフタイムを超えて使うことができない」ということになります。
スライス
スライスはリファレンスであり、別に実体があるものに対する「ビュー」を与えています。所有権は持ちません。
文字列リテラル
文字列リテラルに束縛された変数は &’static str 型と推論されます。
文字列リテラルが示す文字列はプログラム実行の初期にメモリの「静的領域」と呼ばれる領域に配列のように配置され、そのスライスとなります。
「静的領域」はプログラムの起動から最後まで割り当てられていてサイズが不変な「静的変数」を配置する領域です。
構造体
クラスを持つ言語では、クラスの宣言の中で変数やそれを操作するメソッドを定義しますが、Rust では構造体の関連関数やメソッドの定義は別のブロックで行われます。
Box
生ポインタを使わずにヒープ領域にデータを配置できる Box型があります。
Box型を用いるもう1つの場合が、コンパイルのときには方が決まらない場合
トレイト
ある動作に関連するメソッドを列挙したものを「トレイト」と呼んでいます。類似のものにJavaやGo言語の「インターフェース」、Haskellの「型クラス」があります。
型パラメータに加える「トレイトを満たしている」という条件のことを「トレイト境界」と呼びます。
ある型がトレイトを満たすためには、トレイトで宣言されたすべてのメソッドがその型に実装される必要がありますが、デフォルトの実装があるメソッドはすでに実装済みになります。
その他
型やライフパラメータを固定せずにパラメータ化し、構造体や関数・メソッドを定義することをジェネリクス(generics)と呼んでいます。