¿Por qué el uso de mayúsculas en la primera letra de una cadena es tan complicado en Rust?


81

Me gustaría poner en mayúscula la primera letra de a &str. Es un problema simple y espero una solución simple. La intuición me dice que haga algo como esto:

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

Pero los &strcorreos electrónicos no se pueden indexar de esta manera. La única forma en que he podido hacerlo parece demasiado complicada. Convierto el &stren un iterador, convierto el iterador en un vector, en mayúsculas el primer elemento del vector, lo que crea un iterador, en el que indexo, creando un Option, que desenvuelvo para darme la primera letra en mayúscula. Luego convierto el vector en un iterador, que convierto en a String, que convierto en 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;

¿Existe una forma más fácil que esta, y si es así, cuál? Si no es así, ¿por qué Rust está diseñado de esta manera?

Pregunta similar


44
Es un problema simple , no, no lo es. Escriba en mayúscula ßcuando se interprete como alemán. Pista: no es un solo personaje. Incluso el planteamiento del problema puede resultar complicado. Por ejemplo, sería incorrecto poner en mayúscula el primer carácter del apellido von Hagen. Todo esto es un aspecto de vivir en un mundo global que ha tenido miles de años de culturas divergentes con diferentes prácticas y estamos tratando de aplastar todo eso en 8 bits y 2 líneas de código.
Shepmaster

3
Lo que planteas parece ser un problema de codificación de caracteres, no un problema de tipo de datos. Supongo que char :: to_uppercase ya maneja correctamente Unicode. Mi pregunta es, ¿por qué son necesarias todas las conversiones de tipos de datos? Parece que la indexación podría devolver un carácter Unicode de varios bytes (no un carácter de un solo byte, lo que supondría solo ascii), y to_uppercase podría devolver un carácter en mayúsculas en cualquier idioma en el que se encuentre, si hay alguno disponible en dicho idioma.
Marshallm

3
@marshallm de char::to_uppercasehecho maneja este problema, pero desperdicia sus esfuerzos tomando solo el primer punto de código ( nth(0)) en lugar de todos los puntos de código que componen la capitalización

La codificación de caracteres no es un proceso sencillo como señaló Joel en Software: Unicode .
Nathan

@Shepmaster, en general tienes razón. Es un problema simple en inglés (la base estándar de facto de lenguajes de programación y formatos de datos). Sí, hay scripts en los que la "capitalización" ni siquiera es un concepto, y otros en los que es muy complicado.
Paul Draper

Respuestas:


98

¿Por qué es tan complicado?

Vamos a analizarlo, línea por línea

let s1 = "foobar";

Hemos creado una cadena literal codificada en UTF-8 . UTF-8 nos permite codificar los 1,114,112 puntos de código de Unicode de una manera bastante compacta si viene de una región del mundo que escribe principalmente caracteres que se encuentran en ASCII , un estándar creado en 1963. UTF-8 es una longitud variable codificación, lo que significa que un solo punto de código puede ocupar de 1 a 4 bytes . Las codificaciones más cortas están reservadas para ASCII, pero muchos Kanji toman 3 bytes en UTF-8 .

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

Esto crea un vector de characciones. Un carácter es un número de 32 bits que se asigna directamente a un punto de código. Si comenzamos con texto solo ASCII, hemos cuadruplicado nuestros requisitos de memoria. Si tuviéramos un montón de personajes del plano astral , entonces tal vez no hayamos usado mucho más.

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

Esto toma el primer punto de código y solicita que se convierta a una variante en mayúsculas. Desafortunadamente para aquellos de nosotros que crecimos hablando inglés, no siempre hay un mapeo simple uno a uno de una "letra pequeña" a una "letra grande" . Nota al margen: los llamamos mayúsculas y minúsculas porque un cuadro de letras estaba encima del otro cuadro de letras en el día .

Este código entrará en pánico cuando un punto de código no tenga una variante correspondiente en mayúsculas. No estoy seguro de si existen, en realidad. También podría fallar semánticamente cuando un punto de código tiene una variante en mayúsculas que tiene varios caracteres, como el alemán ß. Tenga en cuenta que es posible que ß nunca se escriba con mayúscula en The Real World, este es el ejemplo que siempre puedo recordar y buscar. A partir del 2017-06-29, de hecho, las reglas oficiales de ortografía alemana se han actualizado para que tanto "ẞ" como "SS" sean mayúsculas válidas .

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

Aquí convertimos los caracteres nuevamente a UTF-8 y requerimos una nueva asignación para almacenarlos, ya que la variable original se almacenó en memoria constante para no ocupar memoria en tiempo de ejecución.

let s3 = &s2;

Y ahora tomamos una referencia a eso String.

Es un simple problema

Desafortunadamente, esto no es verdad. ¿Quizás deberíamos esforzarnos por convertir el mundo al esperanto ?

Supongo que char::to_uppercaseya maneja correctamente Unicode.

Sí, ciertamente eso espero. Desafortunadamente, Unicode no es suficiente en todos los casos. Gracias a huon por señalar la I turca , donde tanto la versión mayúscula ( İ ) como la minúscula ( i ) tienen un punto. Es decir, no hay una mayúscula adecuada de la letra i; también depende de la ubicación del texto fuente.

¿Por qué la necesidad de todas las conversiones de tipos de datos?

Porque los tipos de datos con los que está trabajando son importantes cuando le preocupa la corrección y el rendimiento. A chares de 32 bits y una cadena está codificada en UTF-8. Son cosas diferentes.

la indexación podría devolver un carácter Unicode de varios bytes

Puede que haya alguna terminología que no coincida aquí. A char es un carácter Unicode de varios bytes.

Es posible cortar una cadena si va byte a byte, pero la biblioteca estándar entrará en pánico si no se encuentra en un límite de caracteres.

Una de las razones por las que nunca se implementó la indexación de una cadena para obtener un carácter es porque mucha gente hace un mal uso de las cadenas como matrices de caracteres ASCII. Indexar una cadena a establecer un carácter nunca podría ser eficiente: tendría que poder reemplazar de 1 a 4 bytes con un valor que también sea de 1 a 4 bytes, lo que hace que el resto de la cadena rebote bastante.

to_uppercase podría devolver un carácter en mayúsculas

Como se mencionó anteriormente, ßes un carácter único que, cuando se escribe en mayúscula, se convierte en dos caracteres .

Soluciones

Vea también la respuesta de trentcl que solo caracteres ASCII en mayúsculas.

Original

Si tuviera que escribir el código, se vería así:

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

Pero probablemente buscaría mayúsculas o Unicode en crates.io y dejaría que alguien más inteligente que yo lo manejara.

Mejorado

Hablando de "alguien más inteligente que yo", Veedrac señala que probablemente sea más eficiente convertir el iterador nuevamente en un segmento después de acceder a los primeros puntos de código de capital. Esto permite una parte memcpydel resto de los bytes.

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
Después de pensarlo mucho, comprendo mejor estas opciones de diseño. La biblioteca estándar debe elegir las compensaciones más versátiles, eficaces y seguras posibles. De lo contrario, obliga a los desarrolladores a hacer concesiones que podrían no ser apropiadas para su aplicación, arquitectura o ubicación. O podría provocar ambigüedad y malentendidos. Si prefiero otras compensaciones, puedo elegir una biblioteca de terceros o escribirla yo mismo.
marshallm

13
@marshallm, ¡es genial escucharlo! Me temo que muchos recién llegados a Rust malinterpretan las decisiones que han tomado los diseñadores de Rust y simplemente las descartan como demasiado complicadas para ningún beneficio. Al hacer y responder preguntas aquí, he ganado una apreciación por el cuidado que se necesita en tales diseños y espero convertirme en un mejor programador. Mantener la mente abierta y estar dispuesto a aprender más es una gran característica para tener como programador.
Shepmaster

6
La "i turca" es un ejemplo de dependencia local que es más directamente relevante para esta pregunta en particular que la clasificación.
huon

6
Me sorprende que tengan to_uppercase y to_lowercase pero no to_titlecase. IIRC, algunos caracteres Unicode en realidad tienen una variante de título especial.
Tim

6
Por cierto, incluso un solo punto de código puede no ser la unidad adecuada para convertir. ¿Qué pasa si el primer carácter es un grupo de grafemas que debería recibir un manejo especial cuando se escribe en mayúsculas? (Da la casualidad de que las diéresis descompuestas funcionan si simplemente escribes en mayúsculas el carácter base, pero no sé si eso es universalmente cierto).
Sebastian Redl

21

¿Existe una forma más fácil que esta, y si es así, cuál? Si no es así, ¿por qué Rust está diseñado de esta manera?

Bueno, sí y no. Su código, como señaló la otra respuesta, no es correcto y entrará en pánico si le da algo como བོད་ སྐད་ ལ་. Entonces, hacer esto con la biblioteca estándar de Rust es incluso más difícil de lo que pensaba inicialmente.

Sin embargo, Rust está diseñado para fomentar la reutilización de código y facilitar la incorporación de bibliotecas. Entonces, la forma idiomática de poner en mayúscula una cadena es bastante aceptable:

extern crate inflector;
use inflector::Inflector;

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

4
La pregunta del usuario suena más a lo que le gustaría .to_sentence_case().
Christopher Oezbek

1
Lamentablemente, no ayuda a nombrar cosas ... Esta es una biblioteca increíble y nunca la vi antes, pero su nombre es difícil (para mí) de recordar y tiene funciones que casi no tienen nada que ver con la inflexión real, una de ellas siendo tu ejemplo.
Sahsahae

11

No es especialmente complicado si puede limitar su entrada a cadenas solo ASCII.

Desde Rust 1.23, strtiene un make_ascii_uppercasemétodo (en versiones anteriores de Rust, estaba disponible a través del AsciiExtrasgo). Esto significa que puede escribir en mayúsculas secciones de cadenas solo en ASCII con relativa facilidad:

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

Esto se convertirá "taylor"en "Taylor", pero no se convertirá "édouard"en "Édouard". ( patio de recreo )

Úselo con precaución.


2
Ayuda a un novato en Rust, ¿por qué es rmutable? Veo que ses mutable str. Ohhhh ok: tengo la respuesta para mi propia pregunta: get_mut(llamado aquí con un rango) devuelve explícitamente Option<&mut>.
Steven Lu

0

Así es como resolví este problema, observe que tuve que verificar si self no es ascii antes de transformarlo en mayúsculas.

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

Salida

Bruno
B
🦀
ß

བོད་སྐད་ལ 

-1

Aquí hay una versión que es un poco más lenta que la versión mejorada de @ Shepmaster, pero también más idiomática :

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

Lo hice de esta manera:

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

Si no es una cadena ASCII:

fn str_cap(s: &str) -> String {
  format!("{}{}", s.chars().next().unwrap().to_uppercase(), 
  s.chars().skip(1).collect::<String>())
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.