St

Strings en Julia

7 ejercicios

Acerca de Strings

Un String en Julia es una secuencia ordenada de caracteres. Como señala el manual: «Claro que el verdadero problema aparece cuando uno se pregunta qué es un carácter».

Gran parte de esta complejidad se cubrirá con más detalle en el concepto Chars, pero en resumen:

  • Todos los strings en Julia son Unicode.
  • Los strings que además resultan ser ASCII (letras inglesas de la A a la Z en mayúsculas y minúsculas, dígitos y algunos signos de puntuación) son fáciles de manejar.
  • La mayoría de los demás sistemas de escritura del mundo son posibles de manejar, pero requieren más cuidado.

Literales de string

Por lo general, los strings se encierran entre comillas dobles " ". Las comillas simples ' ' solo se usan para los Chars.

También es posible usar comillas dobles triples """ """, lo que permite usar " dentro del string sin escaparlo. Más importante aún, la indentación adicional no deseada se elimina de los strings multilínea.

julia> """
       Multiline
       String
       """
"Multiline\nString\n"

Escapar caracteres

Como la mayoría de los lenguajes desde C en adelante, Julia usa una barra invertida \ para escapar:

  1. Para introducir caracteres que no se imprimen, como el salto de línea \n del ejemplo anterior.
  2. Para proteger caracteres que de otro modo tendrían un significado especial, como \$ (ver más abajo).

Iteración

Un string es una colección iterable, así que un bucle for ... in funciona sin necesidad de convertirlo a un vector de caracteres.

julia> s = "Julia"
"Julia"

# admittedly, this is a silly example
julia> for c in s; print(c, "; "); end
J; u; l; i; a; 

Iterar de esta manera tiene la ventaja de que Julia manejará correctamente los límites de los caracteres, incluso en strings que no son ASCII. La importancia de esto quedará más clara en la siguiente sección.

Indexación

En principio, los strings se pueden indexar exactamente igual que los vectores:

julia> s[1], s[5]
('J', 'a')

Esto claramente funciona para un string ASCII como «Julia», pero ignora la mayoría de los idiomas del mundo.

Considera uno de los caracteres de Beowulf, escrito en inglés antiguo hace unos 15 siglos.

«Hrōðgār» es claramente 7 caracteres, pero no todos son ASCII.

julia> king = "Hrōðgār"
"Hrōðgār"

julia> length(king)
7

julia> isascii(king)
false

# collect() converts to a Char vector:
julia> collect(king)
7-element Vector{Char}:
 'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
 'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
 'ō': Unicode U+014D (category Ll: Letter, lowercase)
 'ð': Unicode U+00F0 (category Ll: Letter, lowercase)
 'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
 'ā': Unicode U+0101 (category Ll: Letter, lowercase)
 'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)

Indexar un string de esta manera empieza bien, pero en algún momento se rompe:

julia> king[3]
'ō': Unicode U+014D (category Ll: Letter, lowercase)

julia> king[4]
ERROR: StringIndexError: invalid index [4], valid nearby indices [3]=>'ō', [5]=>'ð'

El problema es que indexar de esta manera cuenta bytes, que en el caso de los strings que no son ASCII no tienen una relación 1:1 con los caracteres. Un carácter Unicode/UTF-8 puede necesitar hasta 4 bytes para representarlo, y el concepto Chars explorará esto con más detalle.

Construir strings

Concatenación

Muchos lenguajes usan + como operador de concatenación de strings, pero no Julia. Al menos el mensaje de error es útil.

julia> "s1 " + "s2"
ERROR: MethodError: no method matching +(::String, ::String)
The function `+` exists, but no method is defined for this combination of argument types.
String concatenation is performed with * 

julia> "s1 " * "s2"
"s1 s2"

La función string() concatena una cantidad arbitraria de strings.

julia> string("lots ", "of ", "bits")
"lots of bits"

La función join() hace lo mismo y además acepta un vector de strings y los concatena con un separador dado (por defecto "").

julia> join(["lots", "of", "bits"], "_")
"lots_of_bits"

Ten en cuenta que los strings son inmutables y que la concatenación implica copiar. Por eso, las concatenaciones repetidas dentro de un bucle son ineficientes, y es mejor reunir los fragmentos en un vector y unirlos todos al final.

Repetición

Sencillo:

julia> repeat("Abc", 5)
"AbcAbcAbcAbcAbc"

Interpolación

Muchos lenguajes tienen una forma de insertar valores calculados dentro de los strings, aunque hay un desacuerdo extraordinario sobre cuál es la mejor sintaxis.

En Julia, los valores interpolados van precedidos por $.

julia> myvar = 42
42

julia> "myvar evaluates to $myvar"
"myvar evaluates to 42"

julia> r = 5.3
5.3

julia> "A circle of radius $r has area $(π * r^2)"
"A circle of radius 5.3 has area 88.24733763933729"

Los paréntesis son obligatorios cuando omitirlos sería ambiguo, pero son opcionales para un solo identificador seguido de un espacio en blanco.

Para incluir un $ dentro del string, escápalo como \$.

Ten en cuenta la limitación de que la interpolación actualmente no permite dar formato, como el número de decimales que se muestran.

Algunas alternativas son:

  • Envuelve el cálculo en una función round().
  • Fuera del ejecutor de pruebas de Exercism, el paquete PyFormattedStrings emula los f-strings de Python.
  • La macro @sprintf() se describe más abajo.

@sprintf()

Para tener un control más fino sobre el ensamblado de strings, Julia copia la función sprintf de C (y de varios lenguajes posteriores: Wikipedia enumera unos 30).

En Julia, esto se implementa como una macro dentro del módulo Printf, de ahí el prefijo @.

julia> using Printf

julia> r = 5.3
5.3

julia> @sprintf("A circle of radius %.1f has area %.2f", r, π * r^2)
"A circle of radius 5.3 has area 88.25"

Funciones de string

Por simplicidad, esta sección se centrará en las funciones que devuelven un string nuevo y dejan la entrada sin cambios. Muchas tienen una versión equivalente, nombrada con el sufijo !, que modifica la entrada en el lugar.

Longitud

¿Qué tan largo es un string? A veces, depende de cómo lo cuentes.

El caso sencillo son los strings ASCII, donde cada carácter ocupa 1 byte.

julia> string_asc = "Julia"
"Julia"

julia> length(string_asc)
5

julia> sizeof(string_asc)
5

La función length() devuelve el número de caracteres, y la función sizeof() el número de bytes.

Esta distinción se vuelve importante con otros conjuntos de caracteres:

julia> king = "Hrōðgār"
"Hrōðgār"

julia> length(king)
7

julia> sizeof(king)
10

División

Una operación común en programación es tomar un string largo y dividirlo en partes, a partir de un string separador de uno o más caracteres.

En general, usamos la función split(). El separador por defecto es (cualquier) espacio en blanco, pero se pueden especificar otros.

julia> split("my little string")
3-element Vector{SubString{String}}:
 "my"
 "little"
 "string"

julia> split("My_snake_case_string", "_")
4-element Vector{SubString{String}}:
 "My"
 "snake"
 "case"
 "string"

Estos ejemplos dividen un string en strings más pequeños. Dividir un string en caracteres es una operación distinta.

julia> collect("input string")
12-element Vector{Char}:
 'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
 'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
 'p': ASCII/Unicode U+0070 (category Ll: Letter, lowercase)
 'u': ASCII/Unicode U+0075 (category Ll: Letter, lowercase)
 't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
 ' ': ASCII/Unicode U+0020 (category Zs: Separator, space)
 's': ASCII/Unicode U+0073 (category Ll: Letter, lowercase)
 't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
 'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
 'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
 'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
 'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)

Buscar y reemplazar

¿Un string contiene un substring determinado? Por extraño que parezca, hay dos funciones para comprobar esto: occursin() y contains() se diferencian solo en el orden de sus argumentos.

julia> needle = "Julia"
"Julia"

julia> haystack = "Python, Julia and R"
"Python, Julia and R"

julia> occursin(needle, haystack)
true

julia> contains(haystack, needle)
true

Para ser más específico con la posición:

julia> startswith(haystack, "Python")
true

julia> endswith(haystack, "Python")
false

Para obtener los índices start:end de un substring, tenemos varias funciones:

julia> findfirst(needle, haystack)
9:13

Para reemplazar substrings, enumera los cambios con la sintaxis x => y.

julia> replace("abc", "a"=>"b", "b"=>"c", "c"=>"a")
"bca"

Desde Julia 1.7, la función replace puede aceptar una cantidad arbitraria de cambios, con la garantía de que ningún carácter se cambiará más de una vez durante el reemplazo.

La sintaxis x => y se llama pair, un tipo importante en Julia que se cubre con más detalle en el concepto Dicts and Pairs.

Nota: Por simplicidad, todos los ejemplos anteriores usan literales de string. Muchas de las funciones son más generales y también funcionan con expresiones regulares. Volveremos a esto en un concepto posterior.

Espacios en blanco

A menudo necesitamos eliminar los espacios en blanco al inicio o al final de un string.

Las funciones para esto son lstrip() (izquierda), rstrip() (derecha) y strip() (ambos lados).

julia> strip("\n\t  useful_bit\n\n")
"useful_bit"

Cambiar mayúsculas y minúsculas

Estas funciones tienen nombres bastante descriptivos:

julia> s = repeat("aBcD ", 5)
"aBcD aBcD aBcD aBcD aBcD "

julia> uppercase(s)
"ABCD ABCD ABCD ABCD ABCD "

julia> lowercase(s)
"abcd abcd abcd abcd abcd "

julia> titlecase(s)  # Initial letter of each word uppercase
"Abcd Abcd Abcd Abcd Abcd "

Parseo

Convertir números a su representación como string, y viceversa, suele ser útil.

Convertir de número a string es sencillo, aunque una base distinta de la predeterminada (10) necesita un parámetro adicional:

julia> string(42)
"42"

julia> string(42, base=16)
"2a"

julia> string(42, base=2)
"101010"

Parsear un string para obtener un valor numérico es un poco más complicado y tiene más posibilidades de error. Especificar el tipo numérico de destino es obligatorio. Especificar la base es opcional y por defecto es 10.

julia> parse(Int, "42")
42

julia> parse(Float64, "42")
42.0

julia> parse(Int, "42?")
ERROR: ArgumentError: invalid base 10 digit '?' in "42?"

julia> parse(Int, "42 ")  # whitespace may be tolerated
42

Strings especiales

A veces es útil suprimir la interpolación y el escape habituales dentro de los strings. Para eso, antepón raw a la comilla de apertura.

julia> x = 10
10

julia> "x = $x if we use \$ for interpolation"
"x = 10 if we use \$ for interpolation"

julia> raw"x = $x if we use \$ for interpolation"
"x = \$x if we use \\\$ for interpolation"

Las expresiones regulares tienen su propia sintaxis complicada, y Julia ofrece dos tipos de string útiles: r" " para la expresión regular y s" " para trabajar con fragmentos capturados.

Varios otros strings especiales son menos comunes en la programación al estilo de Exercism, aunque se usan mucho al crear bibliotecas.

  • Los strings de versión semántica v"x.y.z" manejan la numeración mayor.menor.parche de las versiones de software.
  • Los literales de byte array b" " omiten los límites de caracteres Unicode y operan a nivel de bytes.
Editar en GitHub El enlace se abre en una ventana o pestaña nueva

Aprende Strings

La práctica está bloqueada

Desbloquea 2 ejercicios más para practicar Strings