Rustで文字列の最初の文字を大文字にするのはなぜそれほど複雑なのですか?


82

の最初の文字を大文字にしたいのですが&str。それは単純な問題であり、私は単純な解決策を望んでいます。直感は私にこのようなことをするように言います:

let mut s = "foobar";
s[0] = s[0].to_uppercase();

ただし&str、このようにsにインデックスを付けることはできません。私がそれを行うことができた唯一の方法は、過度に複雑に思えます。を&strイテレータに変換し、イテレータをベクトルに変換します。ベクトルの最初の項目を大文字にします。これにより、イテレータが作成され、インデックスが作成されますOption。これをアンラップして、大文字の最初の文字を取得します。次に、ベクトルをイテレータに変換しString、それをに変換し、それをに変換します&str

let s1 = "foobar";
let mut v: Vec<char> = s1.chars().collect();
v[0] = v[0].to_uppercase().nth(0).unwrap();
let s2: String = v.into_iter().collect();
let s3 = &s2;

これよりも簡単な方法はありますか?もしそうなら、何ですか?そうでない場合、なぜRustはこのように設計されているのですか?

同様の質問


46
これは単純な問題です—いいえ、そうではありません。ßドイツ語と解釈される場合は大文字にしてください。ヒント:それは単一の文字ではありません。問題の説明でさえ複雑になる可能性があります。たとえば、姓の最初の文字を大文字にすることは不適切von Hagenです。これはすべて、さまざまな慣行を持つ何千年にもわたる多様な文化が存在するグローバルな世界での生活の側面であり、私たちはそれらすべてを8ビットと2行のコードに押しつぶそうとしています。
シェップマスター2016

3
あなたが提起するのは、データ型の問題ではなく、文字エンコードの問題のようです。char :: to_uppercaseはすでにUnicodeを適切に処理していると思います。私の質問は、なぜすべてのデータ型変換が必要なのかということです。インデックス付けはマルチバイトのUnicode文字(ASCIIのみを想定する1バイト文字ではない)を返す可能性があり、to_uppercaseは、その言語で使用可能な場合は、その言語の大文字を返す可能性があるようです。
marshallm 2016

3
@marshallmはchar::to_uppercase確かにこの問題を処理しますがnth(0)、大文字の使用を構成するすべてのコードポイントではなく、最初のコードポイント()のみを使用することでその努力を捨てます

Joel on Software:Unicodeが指摘しているように、文字エンコードは単純なプロセスではありません。
ネイサン

@Shepmaster、一般的にあなたは正しいです。これは英語の単純な問題です(プログラミング言語とデータ形式の事実上の標準ベース)。はい、「大文字」が概念でさえないスクリプトもあれば、非常に複雑なスクリプトもあります。
ポールドレイパー

回答:


101

なぜそんなに複雑なのですか?

行ごとに分解してみましょう

let s1 = "foobar";

UTF-8でエンコードされたリテラル文字列を作成しました。UTF-8を使用すると、1963年に作成された標準であるASCIIで主に文字を入力する世界の地域から来た場合、Unicodeの1,114,112コードポイントをかなりコンパクトな方法でエンコードできます。UTF -8は可変長です。エンコーディング。これは、単一のコードポイントが1〜4バイトかかる可能性があることを意味します。短いエンコーディングはASCII用に予約されていますが、多くの漢字はUTF-8で3バイトを使用します

let mut v: Vec<char> = s1.chars().collect();

これにより、charアクターのベクトルが作成されます。文字は、コードポイントに直接マップされる32ビットの数値です。ASCIIのみのテキストから始めた場合、メモリ要件は4倍になりました。アストラル界のキャラクターがたくさんいる場合は、それほど多くは使用していない可能性があります。

v[0] = v[0].to_uppercase().nth(0).unwrap();

これにより、最初のコードポイントが取得され、大文字のバリアントに変換されるように要求されます。英語を話すように育った私たちにとって残念なことに、「小さな文字」から「大きな文字」への単純な1対1のマッピングが常にあるとは限りません。補足:当時、1つの文字ボックスが他の文字ボックスの上にあったため、大文字と小文字を使用します

コードポイントに対応する大文字のバリアントがない場合、このコードはパニックになります。実際、それらが存在するかどうかはわかりません。また、コードポイントに、ドイツ語などの複数の文字を含む大文字のバリアントがある場合、意味的に失敗する可能性がありますß。実世界では、ßが実際に大文字になることは決してないことに注意してください。これは、私が常に覚えて検索できる単なる例です。2017-06-29の時点で、実際、ドイツ語のスペルの公式ルールが更新され「ẞ」と「SS」の両方が有効な大文字になりました

let s2: String = v.into_iter().collect();

ここでは、文字をUTF-8に変換し直し、実行時にメモリを占有しないように元の変数が定数メモリに格納されていたため、それらを格納するための新しい割り当てが必要です。

let s3 = &s2;

そして今、私たちはそれへの参照を取りStringます。

それは単純な問題です

残念ながら、これは真実ではありません。おそらく、私たちは世界をエスペラントに変えるよう努力すべきでしょうか?

私はchar::to_uppercaseすでにUnicodeを適切に処理していると思います。

はい、私は確かにそう願っています。残念ながら、Unicodeはすべての場合に十分ではありません。トルコ語のIを指摘してくれたhuonに感謝します。ここでは、大文字(İ)と小文字(i)の両方のバージョンにドットが付いています。それは何もありません、ある1つの文字の正しい総額は、ソーステキストのロケールにも依存しますi

なぜすべてのデータ型変換が必要なのですか?

正確性とパフォーマンスが心配な場合は、使用するデータ型が重要であるためです。Acharは32ビットで、文字列はUTF-8でエンコードされています。それらは異なるものです。

インデックス作成は、マルチバイトのUnicode文字を返す可能性があります

ここにいくつかの不一致の用語があるかもしれません。Achar マルチバイトのUnicode文字です。

バイトごとに移動する場合は文字列をスライスできますが、文字の境界にいない場合は標準ライブラリがパニックになります。

文字を取得するために文字列にインデックスを付けることが実装されなかった理由の1つは、非常に多くの人々が文字列をASCII文字の配列として誤用しているためです。文字列にインデックスを付けて文字を設定するのは決して効率的ではありません。1〜4バイトを1〜4バイトの値に置き換える必要があり、文字列の残りの部分がかなりバウンドします。

to_uppercase 大文字を返す可能性があります

上記のように、ßは1文字で、大文字にすると2文字になります。

ソリューション

ASCII文字のみを大文字にするtrentclの回答も参照してください。

元の

コードを書く必要がある場合は、次のようになります。

fn some_kind_of_uppercase_first_letter(s: &str) -> String {
    let mut c = s.chars();
    match c.next() {
        None => String::new(),
        Some(f) => f.to_uppercase().chain(c).collect(),
    }
}

fn main() {
    println!("{}", some_kind_of_uppercase_first_letter("joe"));
    println!("{}", some_kind_of_uppercase_first_letter("jill"));
    println!("{}", some_kind_of_uppercase_first_letter("von Hagen"));
    println!("{}", some_kind_of_uppercase_first_letter("ß"));
}

しかし、私はおそらくcrates.ioで大文字またはユニコードを検索し、私より賢い人にそれを処理させたいと思います

改善

「私より賢い人」と言えば、Veedracは、最初の大文字のコードポイントにアクセスした後で、イテレータをスライスに戻す方がおそらく効率的であると指摘しています。これによりmemcpy、残りのバイトの1つが可能になります。

fn some_kind_of_uppercase_first_letter(s: &str) -> String {
    let mut c = s.chars();
    match c.next() {
        None => String::new(),
        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
    }
}

34
それについてよく考えた後、私はこれらのデザインの選択をよりよく理解しました。標準ライブラリは、可能な限り最も用途が広く、パフォーマンスが高く、安全なトレードオフを選択する必要があります。そうしないと、開発者はアプリケーション、アーキテクチャ、またはロケールに適さない可能性のあるトレードオフを行う必要があります。または、あいまいさや誤解を招く可能性があります。他のトレードオフを好む場合は、サードパーティのライブラリを選択するか、自分で作成できます。
marshallm 2016

13
@marshallm聞いて本当にうれしいです!Rustの初心者の多くは、Rustの設計者が下した決定を誤解し、複雑すぎて何の利益もないと単純に書き留めてしまうのではないかと心配しています。ここで質問したり答えたりすることで、そのような設計に取り掛かる必要のあるケアに感謝し、より良いプログラマーになることを願っています。オープンマインドを保ち、より多くを学ぶことをいとわないことは、プログラマーとして持つべき大きな特徴です。
シェップマスター2016

6
「トルコi」は、ソートよりも、この特定の質問に、より直接的に関連するロケール依存の一例です。
huon 2016

6
to_uppercaseとto_lowercaseはあるが、to_titlecaseはないことに驚いています。IIRC、一部のUnicode文字には、実際には特別なタイトルケースバリアントがあります。
ティム

6
ちなみに、1つのコードポイントでも変換するのに適切な単位ではない場合があります。最初の文字が、大文字の場合に特別な処理を受ける必要がある書記素クラスターである場合はどうなりますか?(基本文字を大文字にするだけで分解されたウムラウトが機能することがありますが、それが普遍的に正しいかどうかはわかりません。)
SebastianRedl18年

23

これよりも簡単な方法はありますか?もしそうなら、何ですか?そうでない場合、なぜRustはこのように設計されているのですか?

ええ、はい、いいえ。他の回答が指摘しているように、あなたのコードは正しくなく、བོད་སྐད་ལ་のようなものを与えるとパニックになります。したがって、Rustの標準ライブラリを使用してこれを行うことは、当初考えていたよりもさらに困難です。

ただし、Rustは、コードの再利用を促進し、ライブラリの取り込みを容易にするように設計されています。したがって、文字列を大文字にする慣用的な方法は、実際には非常に口当たりが良いです。

extern crate inflector;
use inflector::Inflector;

let capitalized = "some string".to_title_case();

4
ユーザーの質問は、彼が望んで.to_sentence_case()いるように聞こえます。
ChristopherOezbek19年

1
悲しいことに、名前を付けるのに役立ちません...これは素晴らしいライブラリで、これまで見たことがありませんが、名前を覚えるのは(私にとっては)難しく、実際の語尾変化とはほとんど関係のない機能があります。あなたの例です。
Sahsahae

11

入力をASCIIのみの文字列に制限できる場合は、特に複雑ではありません。

Rust 1.23以降strmake_ascii_uppercaseメソッドがあります(古いバージョンのRustでは、AsciiExtトレイトから利用できました)。これは、ASCIIのみの文字列スライスを比較的簡単に大文字にできることを意味します。

fn make_ascii_titlecase(s: &mut str) {
    if let Some(r) = s.get_mut(0..1) {
        r.make_ascii_uppercase();
    }
}

これがオンになります"taylor""Taylor"、それは変わりません"édouard""Édouard"。(遊び場

注意して使用してください。


2
Rust初心者を助けてください、なぜr可変ですか?それsは変更可能だと思いますstr。Ohhhh ok:私は自分の質問に対する答えを持っています:(get_mutここでは範囲付きで呼ばれます)は明示的にを返しますOption<&mut>
スティーブン・呂

0

これが私がこの問題を解決した方法です。大文字に変換する前に、自己がASCIIではないかどうかを確認する必要があることに注意してください。

trait TitleCase {
    fn title(&self) -> String;
}

impl TitleCase for &str {
    fn title(&self) -> String {
        if !self.is_ascii() || self.is_empty() {
            return String::from(*self);
        }
        let (head, tail) = self.split_at(1);
        head.to_uppercase() + tail
    }
}

pub fn main() {
    println!("{}", "bruno".title());
    println!("{}", "b".title());
    println!("{}", "🦀".title());
    println!("{}", "ß".title());
    println!("{}", "".title());
    println!("{}", "བོད་སྐད་ལ".title());
}

出力

Bruno
B
🦀
ß

བོད་སྐད་ལ 

-1

これは、@ Shepmasterの改良バージョンよりも少し遅いバージョンですが、より慣用的なバージョンです。

fn capitalize_first(s: &str) -> String {
    let mut chars = s.chars();
    chars
        .next()
        .map(|first_letter| first_letter.to_uppercase())
        .into_iter()
        .flatten()
        .chain(chars)
        .collect()
}

-1

私はそれをこのようにしました:

fn str_cap(s: &str) -> String {
  format!("{}{}", (&s[..1].to_string()).to_uppercase(), &s[1..])
}

ASCII文字列でない場合:

fn str_cap(s: &str) -> String {
  format!("{}{}", s.chars().next().unwrap().to_uppercase(), 
  s.chars().skip(1).collect::<String>())
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.