Dlaczego wielkie litery w pierwszej literze łańcucha są tak zagmatwane w Rust?

82

Chciałbym zacząć wielką literę od a &str. To prosty problem i mam nadzieję na proste rozwiązanie. Intuicja każe mi zrobić coś takiego:

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

Ale &strnie można go indeksować w ten sposób. Jedyny sposób, w jaki byłem w stanie to zrobić, wydaje się zbyt zawiły. Konwertuję na &striterator, konwertuję iterator na wektor, wielkie litery to pierwsza pozycja w wektorze, co tworzy iterator, do którego indeksuję, tworząc Optionznak, który rozwijam, aby uzyskać pierwszą wielką literę. Następnie konwertuję wektor na iterator, który zamieniam na a String, który zamieniam na a &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;

Czy jest łatwiejszy sposób niż ten, a jeśli tak, to co? Jeśli nie, dlaczego Rust został zaprojektowany w ten sposób?

Podobne pytanie

marshallm
źródło
46
To prosty problem - nie, nie jest. W ßprzypadku interpretacji języka niemieckiego należy pisać wielkimi literami . Wskazówka: to nie jest pojedynczy znak. Nawet stwierdzenie problemu może być skomplikowane. Na przykład niewłaściwe byłoby pisanie dużej litery pierwszego znaku nazwiska von Hagen. To wszystko jest aspektem życia w globalnym świecie, w którym od tysięcy lat istniały rozbieżne kultury z różnymi praktykami, a my staramy się zgnieść to wszystko na 8 bitów i 2 linie kodu.
Shepmaster,
3
To, co pozujesz, wydaje się być problemem z kodowaniem znaków, a nie typem danych. Zakładam, że char :: to_uppercase już poprawnie obsługuje Unicode. Moje pytanie brzmi: dlaczego potrzebne są wszystkie konwersje typów danych? Wydaje się, że indeksowanie może zwrócić wielobajtowy znak Unicode (a nie znak jednobajtowy, który zakładałby tylko ascii), a to_uppercase może zwrócić wielką literę w dowolnym języku, w którym jest dostępny, jeśli jest dostępny w tym języku.
Marshallm
3
@marshallm char::to_uppercaserzeczywiście rozwiązuje ten problem, ale odrzucasz jego wysiłki, biorąc tylko pierwszy punkt kodowy ( nth(0)) zamiast wszystkich punktów kodu, które składają się na
Kodowanie znaków nie jest prostym procesem, jak wskazał Joel w Software: Unicode .
Nathan,
@Shepmaster, ogólnie masz rację. To prosty problem w języku angielskim (de facto standardowa baza języków programowania i formatów danych). Tak, są skrypty, w których „wielkie litery” nie są nawet pojęciem, i inne, w których jest bardzo skomplikowane.
Paul Draper

Odpowiedzi:

101

Dlaczego jest tak zagmatwany?

Podzielmy to, linia po linii

let s1 = "foobar";

Stworzyliśmy ciąg literału zakodowany w UTF-8 . UTF-8 pozwala kodować 1,114,112 punktów kodowych z Unicode w sposób, który jest dość zwarta, jeśli pochodzą z regionu świata, że typy w większości znaków znalezionych w kodzie ASCII , standard utworzony w 1963. UTF-8 jest zmienna długość kodowanie, co oznacza, że ​​pojedynczy punkt kodowy może zająć od 1 do 4 bajtów . Krótsze kodowanie jest zarezerwowane dla ASCII, ale wiele Kanji zajmuje 3 bajty w UTF-8 .

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

Tworzy to wektor charaktorów. Znak to 32-bitowa liczba, która jest bezpośrednio mapowana do punktu kodowego. Jeśli zaczęliśmy od tekstu tylko ASCII, czterokrotnie zwiększyliśmy nasze wymagania dotyczące pamięci. Gdybyśmy mieli kilka postaci z planu astralnego , być może nie używaliśmy dużo więcej.

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

Spowoduje to pobranie pierwszego punktu kodowego i zażądanie konwersji na wariant z dużymi literami. Na nieszczęście dla tych z nas, którzy dorastali mówiąc po angielsku, nie zawsze istnieje proste odwzorowanie „małej litery” na „dużą” . Uwaga dodatkowa: nazywamy je dużymi i małymi literami, ponieważ w tamtych czasach jedno pudełko z literami znajdowało się nad drugim .

Ten kod wywoła panikę, gdy punkt kodowy nie ma odpowiadającego mu wariantu z dużymi literami. Właściwie nie jestem pewien, czy takie istnieją. Może również semantycznie zawieść, gdy punkt kodowy ma wariant z wielkimi literami, który ma wiele znaków, na przykład niemiecki ß. Zauważ, że ß może nigdy nie być pisane wielką literą w The Real World, jest to jedyny przykład, który zawsze pamiętam i którego szukam. W rzeczywistości od 2017-06-29 oficjalne zasady pisowni niemieckiej zostały zaktualizowane, aby zarówno „ẞ”, jak i „SS” były poprawnymi wielkimi literami !

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

Tutaj konwertujemy znaki z powrotem do UTF-8 i wymagamy nowej alokacji, aby je przechowywać, ponieważ oryginalna zmienna była przechowywana w stałej pamięci, aby nie zajmować pamięci w czasie wykonywania.

let s3 = &s2;

A teraz odniesiemy się do tego String.

To prosty problem

Niestety to nieprawda. Może powinniśmy podjąć próbę nawrócenia świata na esperanto ?

Zakładam, że char::to_uppercasejuż poprawnie obsługuje Unicode.

Tak, mam taką nadzieję. Niestety, Unicode nie we wszystkich przypadkach wystarcza. Dzięki Huon dla wskazując na tureckiej I , gdzie zarówno górna ( İ ) i małe litery ( I ) wersje mają kropkę. Oznacza to, że nie ma jednej właściwej wielkości litery i; zależy to również od ustawień regionalnych tekstu źródłowego.

dlaczego potrzeba wszystkich konwersji typu danych?

Ponieważ typy danych, z którymi pracujesz, są ważne, gdy martwisz się o poprawność i wydajność. A charma 32 bity, a łańcuch jest zakodowany w formacie UTF-8. To są różne rzeczy.

indeksowanie może zwrócić wielobajtowy znak Unicode

W tym miejscu może występować niedopasowana terminologia. A char to wielobajtowy znak Unicode.

Cięcie łańcucha jest możliwe, jeśli idziesz bajt po bajcie, ale standardowa biblioteka będzie panikować, jeśli nie jesteś na granicy znaków.

Jednym z powodów, dla których indeksowanie łańcucha w celu uzyskania znaku nigdy nie zostało zaimplementowane, jest to, że tak wiele osób niewłaściwie używa ciągów znaków jako tablic znaków ASCII. Indeksowanie ciągu znaków w celu ustawienia znaku nigdy nie mogłoby być wydajne - musiałbyś być w stanie zastąpić 1-4 bajty wartością, która również ma 1-4 bajty, powodując, że reszta ciągu odbija się dość często.

to_uppercase może zwrócić wielką literę

Jak wspomniano powyżej, ßto pojedynczy znak, który po zapisaniu wielkimi literami staje się dwoma znakami .

Rozwiązania

Zobacz także odpowiedź trentcl, która zawiera tylko wielkie litery w znakach ASCII.

Oryginalny

Gdybym miał napisać kod, wyglądałby tak:

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("ß"));
}

Ale prawdopodobnie wyszukałbym wielkie litery lub unicode w crates.io i pozwoliłbym zająć się tym komuś mądrzejszemu ode mnie.

Ulepszony

Mówiąc o „kimś mądrzejszym ode mnie”, Veedrac wskazuje, że prawdopodobnie bardziej wydajne jest przekonwertowanie iteratora z powrotem na plasterek po uzyskaniu dostępu do pierwszych dużych punktów kodu . Pozwala to memcpyna resztę bajtów.

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(),
    }
}
Shepmaster
źródło
34
Po długim przemyśleniu lepiej rozumiem te wybory projektowe. Biblioteka standardowa powinna wybierać możliwie najbardziej wszechstronne, wydajne i bezpieczne kompromisy. W przeciwnym razie zmusza programistów do dokonywania kompromisów, które mogą nie być odpowiednie dla ich aplikacji, architektury lub ustawień regionalnych. Albo może prowadzić do niejasności i nieporozumień. Jeśli wolę inne kompromisy, mogę wybrać bibliotekę innej firmy lub napisać ją samodzielnie.
marshallm
13
@marshallm to naprawdę fajnie to słyszeć! Obawiam się, że wielu nowoprzybyłych do Rust źle zrozumie decyzje, które podjęli projektanci Rusta i po prostu odpisze je jako zbyt skomplikowane, by nie przynosiły korzyści. Zadając tutaj pytania i odpowiadając na nie, zyskałem wdzięczność za troskę, jaką trzeba włożyć w takie projekty i mam nadzieję, że zostanę lepszym programistą. Zachowanie otwartego umysłu i chęć uczenia się więcej to wspaniała cecha programisty.
Shepmaster
6
„Turkish I” jest przykładem locale uzależnienia, które jest bardziej bezpośrednio odnoszące się do tej konkretnej kwestii niż sortowania.
huon
6
Jestem zaskoczony, że mają to_uppercase i to_lowercase, ale nie to_titlecase. IIRC, niektóre znaki Unicode mają w rzeczywistości specjalny wariant tytułu.
Tim
6
Nawiasem mówiąc, nawet pojedynczy punkt kodowy może nie być odpowiednią jednostką do konwersji. Co się stanie, jeśli pierwszy znak jest grafemowym klastrem, który powinien być traktowany w specjalny sposób, gdy dodamy wielkie litery? (Tak się składa, że ​​rozłożone umlauty działają, jeśli użyjesz tylko wielkiej litery podstawowej, ale nie wiem, czy to jest uniwersalna prawda.)
Sebastian Redl
23

Czy jest łatwiejszy sposób niż ten, a jeśli tak, to co? Jeśli nie, dlaczego Rust został zaprojektowany w ten sposób?

Cóż, tak i nie. Twój kod, jak wskazała druga odpowiedź, jest nieprawidłowy i będzie panikować, jeśli podasz mu coś w rodzaju བོད་ སྐད་ ལ་. Zatem zrobienie tego ze standardową biblioteką Rusta jest jeszcze trudniejsze niż początkowo sądziłeś.

Jednak Rust został zaprojektowany, aby zachęcać do ponownego użycia kodu i ułatwiać wprowadzanie bibliotek. Więc idiomatyczny sposób kapitalizacji ciągu jest w rzeczywistości całkiem przyjemny:

extern crate inflector;
use inflector::Inflector;

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

źródło
4
Pytanie użytkownika brzmi bardziej tak, jakby chciał .to_sentence_case().
Christopher Oezbek
1
Niestety nie pomaga to nazywanie rzeczy ... To niesamowita biblioteka i nigdy wcześniej jej nie widziałem, ale jej nazwa jest trudna (dla mnie) do zapamiętania i ma funkcje, które nie mają prawie nic wspólnego z rzeczywistą odmianą, jedną z nich być twoim przykładem.
Sahsahae
11

Nie jest to szczególnie skomplikowane, jeśli możesz ograniczyć swoje dane wejściowe do ciągów znaków ASCII.

Od wersji Rust 1.23 strma make_ascii_uppercasemetodę (w starszych wersjach Rusta była dostępna poprzez AsciiExtcechę). Oznacza to, że możesz ze względną łatwością pisać wielkimi literami w plasterkach zawierających tylko ASCII:

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

To zmieni się "taylor"w "Taylor", ale nie zmieni się "édouard"w "Édouard". ( plac zabaw )

Używaj ostrożnie.

trentcl
źródło
2
Pomóż nowicjuszowi w Rust, dlaczego jest rzmienny? Widzę, że sto zmienna str. Ohhhh ok: Mam odpowiedź na moje własne pytanie: get_mut(zwane tutaj z zakresem) wyraźnie zwraca Option<&mut>.
Steven Lu
0

W ten sposób rozwiązałem ten problem, zauważ, że musiałem sprawdzić, czy self nie jest ascii przed zamianą na wielkie litery.

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());
}

Wynik

Bruno
B
🦀
ß

བོད་སྐད་ལ 
Bruno Rocha - rochacbruno
źródło
-1

Oto wersja, która jest nieco wolniejsza niż ulepszona wersja @ Shepmaster, ale także bardziej idiomatyczna :

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()
}
yuyoyuppe
źródło
-1

Zrobiłem to w ten sposób:

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

Jeśli nie jest to ciąg ASCII:

fn str_cap(s: &str) -> String {
  format!("{}{}", s.chars().next().unwrap().to_uppercase(), 
  s.chars().skip(1).collect::<String>())
}
Nikolai Lasunov
źródło