Rangos

Rangos en Kotlin

Una expresión de rango consiste en una sucesión de valores entre un límite inferior y un límite superior; se trata de un intervalo de un conjunto finito definido entre un valor de inicio y un valor final.

En esta entrada vamos a repasar algunas fórmulas para crear distintos tipos de rangos y para recorrer y verificar sus elementos, y veremos algunos atributos y operaciones aplicables a los rangos.

Los rangos son aplicables a cualquier tipo comparable, pero se implementa habitualmente de forma optimizada con algunos tipos primitivos como Int, Long y Char porque los rangos de estos tipos, y específicamente de números enteros, tienen una característica adicional: pueden ser iterados como si fueran un bucle indexado. Por eso en muchas ocasiones se utilizan asociados a estructuras de repetición o bucles.

En Kotlin los rangos son inclusivos por defecto, esto es, incluyen los valores mínimo y máximo. Y la distancia (paso o step) entre dos valores está definida por defecto en 1.

Básicamente en Kotlin existen tres maneras de crear rangos:

1. Con la función rangeTo():
for (i in 1.rangeTo(5)) print(i) // 12345
2. Con el operador ..  que es la forma más común:
val unoAcinco = 1..5
for (n in unoAcinco) print(n) // 12345

for (i in 1..5) print(i) // 12345

for (i in 5..1) print(i) // no imprime nada
3. Con la función downTo() que itera en orden inverso o descendente:
for (i in 5.downTo(1)) print(i) // 54321

for (i in 5 downTo 1) print(i) // 54321
Otra forma de conseguir el mismo resultado:
for (i in (1..5).reversed()) print(i) // 54321

También se puede crear un rango siguiendo un paso arbitrario distinto de 1 (por defecto) con la función step():
for (i in 1..5 step 2) print(i) // 135

for (i in 5 downTo 1 step 2) print(i) // 531

// obtiene los números pares desde 100 hasta 1
val cienAUno = 100 downTo 1
for (i in cienAUno step 2) {
    println(i)
}

Para crear un rango que no incluya su elemento final, se puede usar la función until:
for (i in 1 until 10) print(i) // 123456789

También podemos crear rangos de caracteres:
val aToZ = "a".."z"

for (c in 'a'..'k') print("$c ") // a b c d e f g h i j k

for (c in 'k' downTo 'a') print("$c ") // k j i h g f e d c b a

Podemos usar la función forEach() para recorrer el rango de valores:
(1..5).forEach(::print) // 12345

(1..5).reversed().forEach { e -> print("$e ") } // 5 4 3 2 1

(1..9).reversed().forEach {
    print(it) // 987654321
}

(1..9).reversed().step(3).forEach {
    print(it) // 963
}

Otra opción es usar iterator() para recorrer un rango:
val chars = ('a'..'f')
val it = chars.iterator()

while (it.hasNext()) {
    val elemento = it.next()
    print(elemento) // abcdef
}
En el código anterior, después de crear un rango de caracteres y un iterador a partir de ese rango, creamos un bucle while para recorrer el rango cuyo cuerpo se ejecuta mientras el método hasNext() verifique si hay un elemento siguiente en el rango, y en ese caso, el método next() recupera ese elemento, que es asignado a la variable.

El operador in (y su opuesto !in) es usado para verificar si un valor está presente en un rango dado:
if (5 in 1..10) print("5 está en el rango")

fun esLetraMayuscula (letra:Char) = letra in 'A'..'Z'
    println(esLetraMayuscula('b')) //false
    println(esLetraMayuscula('Q')) //true
}

val x = 4
when (x) {
    in 1..10 -> print("x está en el rango")
    !in 10..20 -> print("x no está en el rango")
}

Y aunque no podemos usar Strings para crear rangos (necesitamos conjuntos finitos), sí podemos comprobar si un elemento de este tipo está entre dos Strings:
fun estaEntreStrings (cadena: String) = cadena in "Java" .. "Kotlin"
println(estaEntreStrings("Javascript")) // true
println(estaEntreStrings("Python")) // false

Como conjunto o almacén de valores, los rangos tienen ciertas semejanzas con las colecciones (sobre las que trataremos en entradas posteriores) y comparten con ellas algunas características y operaciones. Por una parte, además del atributo de paso (step), un rango en Kotlin tiene los atributos first y last que devuelven el primer y último valor respectivamente.
println((1..10 step 4).step) // 4
println((1..10 step 3).first) // 1
println((1..10 step 2).last) // 9
Y por otra parte, a los rangos también se les puede aplicar ciertas operaciones con los métodos filter, reduce y map:
val nums = (1..15)

// filter selecciona los números pares y devuelve una lista con esos valores
println(nums.filter { e -> e % 2 == 0 })
// [2, 4, 6, 8, 10, 12, 14]

// map aplica la operación a cada elemento y devuelve una lista con los nuevos valores
println(nums.map { e -> e * 5 })
// [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75]

// reduce aplica la expresión a cada elemento y devuelve un solo valor:
println(nums.reduce { total, next -> total + next }) // 120
Además existen otras operaciones predefinidas para devolver un valor determinado (min, max, sum, average, count), para ordenar los elementos (sorted) y para crear una lista sin elementos repetidos (distinct):
val r = (1..10)
println(r.min()) // 1
println(r.max()) // 10
println(r.sum()) // 55
println(r.average()) // 5.5
println(r.count()) // 10

val s = (1..10 step 2)
val t = (r + s)
for (i in t) print(i) // 1234567891013579
for (i in t.sorted()) print(i) // 1123345567789910
print(t.distinct()) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Comentarios

Entradas populares

I/O: entrada y salida de datos en consola

Recursos gratis para aprender Kotlin

Lectura y escritura de archivos