Scala

Manejo de números en Scala

9 min lectura José Miguel

Introducción

En este artículo vamos a aprender sobre el manejo de números en Scala. En Scala, los tipos Byte, Short, Int, Long y Char se conocen como tipos integrales porque están representados por enteros o números enteros. Los tipos integrales junto con Double y Float comprenden los tipos numéricos de Scala. Estos tipos numéricos amplían el trait AnyVal, al igual que los tipos Boolean y Unit.

Jerarquía de tipos en Scala

La relación de los tipos de valores predefinidos con AnyVal y Any (así como Nothing) se muestra en la siguiente figura.

Como se muestra en la imagen:

  • Todos los tipos numéricos amplían AnyVal.
  • Todos los demás tipos en la jerarquía de clases de Scala amplían AnyRef.

La siguiente tabla resume los tipos numéricos de Scala, su tamaño en bits y el rango de valores que pueden almacenar:

Tipo Tamaño (bits) Rango
Byte 8 -128 a 127
Short 16 -32,768 a 32,767
Int 32 -2,147,483,648 a 2,147,483,647
Long 64 -9,223,372,036,854,775,808 a 9,223,372,036,854,775,807
Float 32 ±3.4028235E38 (precisión ~7 dígitos)
Double 64 ±1.7976931348623157E308 (precisión ~16 dígitos)

Rangos de datos en Scala REPL

Si alguna vez necesita saber los valores exactos de los rangos de datos puede encontrarlos en Scala REPL de la siguiente forma:

scala> Short.MinValue
val res0: Short = -32768

scala> Short.MaxValue
val res1: Short = 32767

scala> Int.MinValue
val res2: Int = -2147483648

scala> Int.MaxValue
val res3: Int = 2147483647

scala> Long.MinValue
val res4: Long = -9223372036854775808

scala> Long.MaxValue
val res5: Long = 9223372036854775807

scala> Float.MinValue
val res6: Float = -3.4028235E38

scala> Float.MaxValue
val res7: Float = 3.4028235E38

scala> Double.MinValue
val res8: Double = -1.7976931348623157E308

scala> Double.MaxValue
val res9: Double = 1.7976931348623157E308

Guiones bajos en literales numéricos

Scala 2.13 introdujo la capacidad de usar guiones bajos en valores literales numéricos, veamos algunos ejemplos:

val x = 1_000_000       // Int: 1000000
val y = 1_000_000L      // Long: 1000000
val z = 1_000_000.00    // Double: 1000000.0
val a = 1_000_000.00f   // Float: 1000000.0

val hex = 0xFF_FF_FF    // Int en hexadecimal: 16777215
val bin = 0b1000_0000   // Int en binario: 128

Esta característica mejora enormemente la legibilidad del código cuando trabajamos con cantidades grandes, ya que permite separar visualmente los miles, millones, etc., sin afectar el valor numérico real.

¿Cómo crear un número a partir de un String?

Puede que en muchas ocasiones deseemos convertir un String a uno de los tipos numéricos de Scala. Para poder realizar esto debemos usar los métodos to* que están disponibles en un String. Veamos cómo realizarlo:

scala> "1".toInt
val res0: Int = 1

scala> "1".toByte
val res1: Byte = 1

scala> "1".toShort
val res2: Short = 1

scala> "1".toLong
val res3: Long = 1

scala> "1.5".toFloat
val res4: Float = 1.5

scala> "1.5".toDouble
val res5: Double = 1.5

Debemos tener cuidado porque estos métodos pueden generar una excepción del tipo NumberFormatException. Veamos un ejemplo:

scala> "hello".toInt
java.lang.NumberFormatException: For input string: "hello"

scala> "1.5".toInt
java.lang.NumberFormatException: For input string: "1.5"

Es posible que se prefiera utilizar los métodos to*Option, que devuelven Some cuando la conversión es exitosa y None cuando la conversión falla:

scala> "1".toIntOption
val res0: Option[Int] = Some(1)

scala> "hello".toIntOption
val res1: Option[Int] = None

scala> "1.5".toDoubleOption
val res2: Option[Double] = Some(1.5)

scala> "abc".toDoubleOption
val res3: Option[Double] = None

El uso de los métodos to*Option es una práctica recomendada en Scala, ya que nos permite manejar las conversiones fallidas de forma segura sin necesidad de capturar excepciones con try/catch. Esto es especialmente útil cuando procesamos datos de entrada del usuario o datos provenientes de fuentes externas donde no podemos garantizar el formato.

Conversión entre tipos numéricos (casting)

A menudo cuando trabajemos con el manejo de números en Scala tendremos que convertir de un tipo numérico a otro, como de Int a Double, Double a Int, o posiblemente una conversión que involucre BigInt o BigDecimal.

Los valores numéricos normalmente se convierten de un tipo a otro con una colección de métodos to*, incluidos toByte, toChar, toDouble, toFloat, toInt, toLong y toShort.

Los valores numéricos se convierten fácilmente en la dirección de menor a mayor precisión:

ByteShortIntLongFloatDouble

Es importante tener en cuenta que las conversiones en la dirección contraria (de mayor a menor precisión) pueden resultar en pérdida de datos. Por ejemplo, convertir un Double a Int trunca la parte decimal, y convertir un Long grande a Int puede producir un valor incorrecto por desbordamiento.

Veamos algunos ejemplos:

scala> val i = 42
val i: Int = 42

scala> i.toDouble
val res0: Double = 42.0

scala> i.toLong
val res1: Long = 42

scala> i.toFloat
val res2: Float = 42.0

scala> val d = 3.14159
val d: Double = 3.14159

scala> d.toInt
val res3: Int = 3

scala> d.toLong
val res4: Long = 3

scala> d.toFloat
val res5: Float = 3.14159

asInstanceOf

Dependiendo de las necesidades, podemos hacer un cast usando asInstanceOf:

scala> val i = 42
val i: Int = 42

scala> i.asInstanceOf[Double]
val res0: Double = 42.0

scala> i.asInstanceOf[Long]
val res1: Long = 42

scala> i.asInstanceOf[Byte]
val res2: Byte = 42

Nota: En general, se recomienda usar los métodos to* en lugar de asInstanceOf para conversiones numéricas, ya que asInstanceOf es un cast de bajo nivel que puede provocar errores en tiempo de ejecución si se usa incorrectamente, mientras que los métodos to* son más seguros y expresivos.

¿Cómo anular el tipo numérico predeterminado?

Cuando se utiliza un estilo de declaración de tipo implícito, Scala asigna automáticamente los tipos de datos en función de sus valores numéricos. Puede suceder que deseemos anular el tipo predeterminado cuando creamos un campo numérico. Por ejemplo, si asigna 1 a una variable sin declarar explícitamente su tipo, Scala le asigna el tipo Int:

scala> val x = 1
val x: Int = 1

scala> val y = 1.5
val y: Double = 1.5

Por lo tanto, cuando necesite controlar el tipo, declárelo explícitamente de la siguiente forma:

val b: Byte = 1
val s: Short = 1
val l: Long = 1
val f: Float = 1.0f
val d: Double = 1.0

Para longs, doubles y floats también puedes usar este estilo:

val l = 1L       // Long
val d = 1.0D     // Double (explícito, aunque 1.0 ya es Double por defecto)
val f = 1.0F     // Float

Esta segunda forma es más concisa y es especialmente útil cuando queremos dejar claro el tipo sin recurrir a una anotación de tipo completa. Es habitual ver este estilo en código Scala que opera con literales numéricos de distintos tipos.

Manejo de números grandes

El manejo de números en Scala puede ser algo complicado si está escribiendo una aplicación y necesita usar valores enteros o decimales muy grandes.

Si estamos en esta situación y los tipos Long y Double no son lo suficientemente grandes, podemos usar las clases de Scala BigInt y BigDecimal. Veamos a continuación un ejemplo:

scala> val b = BigInt(1234567890123456789L)
val b: scala.math.BigInt = 1234567890123456789

scala> val bd = BigDecimal(123456.789)
val bd: scala.math.BigDecimal = 123456.789

scala> val big = BigInt("99999999999999999999999999999")
val big: scala.math.BigInt = 99999999999999999999999999999

BigInt y BigDecimal admiten todos los operadores que utilizamos con tipos numéricos en Scala:

scala> val a = BigInt(100)
val a: scala.math.BigInt = 100

scala> val b = BigInt(200)
val b: scala.math.BigInt = 200

scala> a + b
val res0: scala.math.BigInt = 300

scala> a * b
val res1: scala.math.BigInt = 20000

scala> a - b
val res2: scala.math.BigInt = -100

scala> b / a
val res3: scala.math.BigInt = 2

scala> b % a
val res4: scala.math.BigInt = 0

scala> b > a
val res5: Boolean = true

Además, podemos convertirlos a otros tipos numéricos:

scala> val b = BigInt(1234)
val b: scala.math.BigInt = 1234

scala> b.toInt
val res0: Int = 1234

scala> b.toLong
val res1: Long = 1234

scala> b.toDouble
val res2: Double = 1234.0

scala> b.toFloat
val res3: Float = 1234.0

scala> val bd = BigDecimal(3.14159)
val bd: scala.math.BigDecimal = 3.14159

scala> bd.toInt
val res4: Int = 3

scala> bd.toDouble
val res5: Double = 3.14159

Es importante recordar que al convertir un BigInt o BigDecimal a un tipo más pequeño (como Int o Long), podemos sufrir pérdida de datos si el valor excede el rango del tipo destino. En estos casos, Scala no lanza una excepción sino que trunca el valor silenciosamente, lo cual puede llevar a errores difíciles de detectar.

¿Cómo generar números aleatorios?

En algunas situaciones puede darse el caso de que necesitemos crear números aleatorios, como cuando probamos una aplicación, realizamos una simulación o cualquier otra situación.

En Scala podemos crear números aleatorios con la clase scala.util.Random. Los siguientes ejemplos muestran casos de uso comunes de generación de números aleatorios:

scala> import scala.util.Random

// Número entero aleatorio
scala> val r = Random.nextInt()
val r: Int = -1453728456

// Número entero aleatorio entre 0 (inclusive) y 100 (exclusive)
scala> val r = Random.nextInt(100)
val r: Int = 47

// Número Double aleatorio entre 0.0 y 1.0
scala> val r = Random.nextDouble()
val r: Double = 0.7254382937

// Número Float aleatorio entre 0.0 y 1.0
scala> val r = Random.nextFloat()
val r: Float = 0.31458

// Booleano aleatorio (true o false)
scala> val r = Random.nextBoolean()
val r: Boolean = true

// Número Long aleatorio
scala> val r = Random.nextLong()
val r: Long = 4523876918274659812

Si necesitamos resultados reproducibles (por ejemplo, para pruebas unitarias), podemos crear una instancia de Random con una semilla fija:

scala> val rng = new Random(42)
val rng: scala.util.Random = scala.util.Random@5a2e4553

scala> rng.nextInt(100)
val res0: Int = 0

scala> rng.nextInt(100)
val res1: Int = 68

Al usar la misma semilla, la secuencia de números generados siempre será la misma, lo que resulta muy útil para escribir tests determinísticos.

Conclusión

En este artículo hemos explorado las principales operaciones de manejo de números en Scala, desde los tipos numéricos básicos (Byte, Short, Int, Long, Float, Double) hasta las clases para números de precisión arbitraria (BigInt, BigDecimal). Aprendimos cómo consultar los rangos de cada tipo, cómo convertir entre String y tipos numéricos de forma segura con los métodos to*Option, cómo realizar casting entre tipos, y cómo generar números aleatorios con scala.util.Random.

Dominar el manejo de números es fundamental para cualquier desarrollador Scala, ya que estas operaciones aparecen constantemente en el desarrollo de aplicaciones, desde el procesamiento de datos con Apache Spark hasta la implementación de algoritmos y lógica de negocio.

José Miguel Moya Curbelo
José Miguel Moya Curbelo
Senior Data Engineer & Big Data Instructor

MSc Applied Mathematics · AWS Cloud Practitioner · SCRUM Master. Especializado en arquitecturas de datos de alto rendimiento con Apache Spark, Snowflake, Python y Scala.

Conectar en LinkedIn

Artículos Relacionados

Deja un comentario

Tu dirección de correo electrónico no será publicada.