Redimensión de imágenes proporcionalmente en PHP

Una necesidad que surge en todas las webs cuyos contenidos son introducidos por un usuario (o varios) a través de un gestor de contenidos, es adaptar las imágenes que estos suben al diseño de la misma para que no se deforme. Además, las proporciones de una imagen en una sección pueden ser diferentes a cómo se muestra en otra; un caso típico es usar una proporción para un listado y otra para la ficha de cada elemento, por ejemplo las noticias, los productos, etc.

Para resolver este problema sirve el conjunto de clases PHP que presentó en este artículo. Este software, a partir de una imagen original, creará una copia por cada tamaño que necesitemos. El proyecto completo puede verse en Github y el objetivo del presente artículo es explicar cómo funciona. En primer lugar, instanciamos la clase:

$objResize = Signia_ImageResize_Factory::getInstanceOf($srcFile, $destFile, $newSize);

Donde:

  • $srcFile es el path hacia la imagen que ha subido el usuario.
  • $desFile es el path de la imagen de destino.
  • $newSize es el tamaño deseado.

A partir de un array con los tamaños deseados podemos crear un bucle por cada uno de ellos ($newSize). Los índices widthMaxheigthMax hacen referencia al tamaño máximo permitido, mientras que width y height pueden entenderse como widthMin y heightMin. Veamos un ejemplo:

$imageType = [
  'slide' => ['width' => 1000, 'widthMax' => 1000, 'height' => 400, 'heightMax' => 400, 'background' => '000000'],
  'r2_34' => ['width' => 468, 'widthMax' => 2340, 'height' => 200, 'heightMax' => 1000],
  'r2_7'  => ['width' => 1080, 'widthMax' => 2700, 'height' => 400, 'heightMax' => 1000],
  'r1_6'  => ['width' => 459, 'widthMax' => 1000, 'height' => 287, 'heightMax' => 625],
  'r1'    => ['width' => 266, 'widthMax' => 266, 'height' => 177, 'heightMax' => 177]
];

En Factory.php podemos ver la clase Signia_ImageResize_Factory:

class Signia_ImageResize_Factory
{
	static public function getInstanceOf($srcImageName, $destImageName, $newSize)
	{
		$aux       = explode(".", $destImageName);
		$extension = end($aux);
		if (preg_match("/jpg|JPG|jpeg|JPEG/", $extension)) {
			$extension = "jpeg";
		}
		$imageResizer = "Signia_ImageResize_" . ucfirst($extension);

		return new $imageResizer($srcImageName, $destImageName, $newSize);
	}
}

Su cometido es invocar la clase que corresponda según la extensión del fichero: Signia_ImageResize_Jpeg, Signia_ImageResize_Gif, etc. Todas ellas tienen el mismo objetivo: crear un recurso imagen a partir del fichero para, más adelante, poder manipular el tamaño de la imagen. Observemos el código de una de ellas, Signia_ImageResize_Jpeg:

class Signia_ImageResize_Jpeg extends Signia_ImageResize_Abstract
{
	function getResizedImage()
	{
		if (!file_exists($this->srcImageName)) {
                    return $this->error_message;
                }
      
                $this->srcImage = @imagecreatefromjpeg($this->srcImageName);
                if ($this->initCheck() && $this->sizeControl()) {
                      $this->destImage = imagecreatetruecolor($this->destWidth, $this->destHeight);
                      if ($this->background) {
                            $this->resizeImageWithBackground();
                      }else{
                           $this->resizeImage();
                      }
                      imagejpeg($this->destImage, $this->destImageName, 50);
         
                      return true;
                }		
	}
}

El tercer parámetro de imagejpeg() es un número entre 0 y 100 que especifica la calidad de la imagen. Obviamente, una mayor calidad implica también un fichero más grande. Google da mucha importancia a la velocidad de carga del sitio a la hora de situarlo entre los resultados de búsqueda, y con 50 se consigue un fichero de peso reducido sin sacrificar demasiada calidad. Esto es especialmente importante si tenemos en cuenta que muchos usuarios descuidan este tema, pudiendo incluso llegar a subir imágenes de más de un 1 MB (siendo esta práctica compatible con la quejas acerca de la velocidad de carga de su sitio).

Si el lector observa las clases Signia_ImageResize_Png y Signia_ImageResize_Gif, comprobará que hay unos extras para que, a pesar de la manipulación de la imagen, no se pierda la transparencia, efecto que sólo puede existir en estos dos formatos de imagen.

class Signia_ImageResize_Gif extends Signia_ImageResize_Abstract
{
   function getResizedImage()
   {
      if (!file_exists($this->srcImageName)) {
         return $this->error_message;
      }
      $this->srcImage = imagecreatefromgif($this->srcImageName);
      if ($this->initCheck() && $this->sizeControl()) {
         $transparency = imagecolortransparent($this->srcImage);
         if ($transparency != -1) {
            $this->destImage    = imagecreatetruecolor($this->aResult[0], $this->aResult[1]);
            $colorTransparent   = imagecolorsforindex($this->srcImage, $transparency);
            $idColorTransparent = imagecolorallocatealpha($this->destImage, $colorTransparent['red'], $colorTransparent['green'], $colorTransparent['blue'], $colorTransparent['alpha']);
            imagefill($this->destImage, 0, 0, $idColorTransparent);
            imagecolortransparent($this->destImage, $idColorTransparent);
         } else {
            $this->destImage = imagecreatetruecolor($this->aResult[0], $this->aResult[1]);
         }
         if ($this->background) {
            $this->resizeImageWithBackground();
         }else{
            $this->resizeImage(true);
         }
         imagegif($this->destImage, $this->destImageName);
         return true;
      }
   }
}

Es desde estas clases donde se llama a la parte más compleja del código, la que realmente redimensiona la imagen: Abstract.php. Básicamente, la imagen que sube el usuario puede diferir en cuanto a las proporciones deseadas en si el ratio de la imagen original es menor o mayor. De no diferenciar el proceso para actuar de forma disitinta en cada caso, algunas imágenes se deformarán, tal y como pasa en los ejemplos que he visto en Stack Overflow sobre el tema. Para entender cómo funciona, bastará con explicar uno sólo de los casos: cuando el destino es mayor. Primero, veamos el contenido de la clase:

abstract class Signia_ImageResize_Abstract
{

   protected $srcImage;
   protected $srcImageName;
   protected $srcWidth;
   protected $srcHeight;
   protected $srcRatio;
   protected $destImage;
   protected $destImageName;
   protected $destWidth;
   protected $destHeight;
   protected $destRatio;
   protected $widthMin;
   protected $heightMin;
   protected $widthMax;
   protected $heightMax;
   protected $background;
   protected $bincolor;
   protected $error;
   public $error_message;
   protected $aMessages = array(
       'src_not_found'      => "El fichero de origen no se encuentra",
       'not_enought_params' => "No se han definido los tamaños de salida",
       'size_values'        => "La imagen es demasiado pequeña para generar las copias",
       'ratio'              => "No se ha podido calcular el ratio destino de la copia"
   );
   protected $aResult   = [];

   public function __construct($srcImageName, $destImageName, $newSize)
   {
      $this->srcImageName   = $srcImageName;
      $this->destImageName  = $destImageName;
      $this->widthMin       = (!isset($newSize['width'])) ? 1 : $newSize['width'];
      $this->heightMin      = (!isset($newSize['height'])) ? 1 : $newSize['height'];
      $this->widthMax       = (!isset($newSize['widthMax']) || $newSize['widthMax'] > 8192) ? 8192 : $newSize['widthMax'];
      $this->heightMax      = (!isset($newSize['heightMax']) || $newSize['heightMax'] > 6144) ? 6144 : $newSize['heightMax'];
      $this->background     = (!isset($newSize['background'])) ? null : $newSize['background'];
      $this->sizeControlled = (!isset($newSize['sizeControlled'])) ? true : $newSize['sizeControlled'];
      if ($this->background) {
         $this->setBincolor();
      }
   }

   public function __destruct()
   {
      if (isset($this->destImage) && is_resource($this->destImage)) {
         imagedestroy($this->destImage);
      }
      if (isset($this->srcImage) && is_resource($this->srcImage)) {
         imagedestroy($this->srcImage);
      }
   }

   /**
    * Si ponemos minWidth y width al mismo valor, e igualamos también minHeight y Height,
    * y la imagen que se sube no es del mismo ratio, saldrán franjas por los lados o arriba y abajo.
    * El background indica el color de estas franjas.
    */
   protected function resizeImageWithBackground()
   {
      if ($this->srcRatio > $this->destRatio) {
         $aux1 = floor($this->srcWidth / $this->destWidth * ($this->destHeight - $this->destWidth / $this->srcWidth * $this->srcHeight));
         $aux2 = floor(($this->destHeight - $this->destWidth / $this->srcWidth * $this->srcHeight) / 2);
         imagecopyresampled($this->destImage, $this->srcImage, 0, 0, 0, -$aux1 / 2, $this->destWidth, $this->destHeight, $this->srcWidth, $this->srcHeight + $aux1);
         $aux3 = imagecolorallocate($this->destImage, $this->bincolor[0], $this->bincolor[1], $this->bincolor[2]);
         imagefilledrectangle($this->destImage, 0, 0, $this->destWidth, $aux2 + 1, $aux3);
         imagefilledrectangle($this->destImage, 0, $this->destHeight - $aux2 - 1, $this->destWidth, $this->destHeight, $aux3);
      } elseif ($this->srcRatio < $this->destRatio) {
         $aux1 = floor($this->srcHeight / $this->destHeight * ($this->destWidth - $this->destHeight / $this->srcHeight * $this->srcWidth));
         $aux2 = floor(($this->destWidth - $this->destHeight / $this->srcHeight * $this->srcWidth) / 2);
         imagecopyresampled($this->destImage, $this->srcImage, 0, 0, -$aux1 / 2, 0, $this->destWidth, $this->destHeight, $this->srcWidth + $aux1, $this->srcHeight);
         $aux3 = imagecolorallocate($this->destImage, $this->bincolor[0], $this->bincolor[1], $this->bincolor[2]);
         imagefilledrectangle($this->destImage, 0, 0, $aux2 + 1, $this->destHeight, $aux3);
         imagefilledrectangle($this->destImage, $this->destWidth - $aux2 - 1, 0, $this->destWidth, $this->destHeight, $aux3);
      }
   }

   /**
    * @param boolean $transparent Para preservar transparencia (imágenes gif y png)
    */
   protected function resizeImage($transparent = false)
   {
      if ($this->srcRatio > $this->destRatio) { // En el original hay más width por cada píxel de height.
         $canvas = imagecreatetruecolor($this->destHeight * $this->srcRatio, $this->destHeight);
         if ($transparent) {
            imagealphablending($canvas, false);
            imagesavealpha($canvas, true);
         }
         $destWidth = $this->destHeight * $this->srcRatio;
         // $canvas contendrá la imagen original con el height del destino y el width recortado proporcionalmente según srcRatio:
         imagecopyresampled($canvas, $this->srcImage, 0, 0, 0, 0, $destWidth, $this->destHeight, $this->srcWidth, $this->srcHeight);
         $auxWidth  = $this->destHeight * $this->srcRatio; // $canvas no es recortada desde x = 0, es decir, no se recorta sólo por la derecha sino también por la izquierda
         imagecopyresampled($this->destImage, $canvas, 0, 0, ($auxWidth - $this->destWidth) / 2, 0, $this->destWidth, $this->destHeight, $this->destWidth, $this->destHeight);
      } elseif ($this->srcRatio < $this->destRatio) { // Menos width por cada height en el original.
         $inverseSrcRatio = $this->srcHeight / $this->srcWidth;
         $canvas          = imagecreatetruecolor($this->destWidth, $this->destWidth * $inverseSrcRatio);
         if ($transparent) {
            imagealphablending($canvas, false);
            imagesavealpha($canvas, true);
         }
         imagecopyresampled($canvas, $this->srcImage, 0, 0, 0, 0, $this->destWidth, $this->destWidth * $inverseSrcRatio, $this->srcWidth, $this->srcHeight);
         $auxHeight = $this->destWidth * $inverseSrcRatio;
         imagecopyresampled($this->destImage, $canvas, 0, 0, 0, ($auxHeight - $this->destHeight) / 2, $this->destWidth, $this->destHeight, $this->destWidth, $this->destHeight);
      } else {
         imagecopyresampled($this->destImage, $this->srcImage, 0, 0, 0, 0, $this->destWidth, $this->destHeight, $this->srcWidth, $this->srcHeight);
      }
   }

   protected function initCheck()
   {
      if (!file_exists($this->srcImageName)) {
         $this->error         = true;
         $this->error_message = $this->aMessages['src_not_found'];
         return false;
      }
      if (!$this->widthMax && !$this->heightMax && !$this->widthMin && !$this->heightMin) {
         $this->error         = true;
         $this->error_message = $this->aMessages['not_enought_params'];
         return false;
      }
      if (!is_resource($this->srcImage)) {
         return false;
      }
      $this->srcWidth  = imagesx($this->srcImage);
      $this->srcHeight = imagesy($this->srcImage);
      $this->error     = false;
      return true;
   }

   protected function sizeControl()
   {
      if (!isset($this->background) && $this->sizeControlled) {
         if ($this->srcWidth < $this->widthMin || $this->srcHeight < $this->heightMin) {
            $this->error         = true;
            $this->error_message = $this->aMessages['size_values'];
            return false;
         }
      }
      // Resolución de incongruencias del tamaño solicitado en $imageType
      if ($this->widthMin > $this->widthMax) {
         $this->widthMin = $this->widthMax;
      }
      if ($this->heightMin > $this->heightMax) {
         $this->heightMin = $this->heightMax;
      }
      // Fin resolución.
      $widthAux  = $this->srcWidth;
      $heightAux = $this->srcHeight;
      //Primero recorta por el ancho:
      if ($this->srcWidth > $this->widthMax) {
         $widthAux  = $this->widthMax;
         $heightAux *= $this->widthMax / $this->srcWidth;
      }
      /* Después por el alto. Si la altura inicial es inferior a la mínima después 
       * de haber sido recortada heightAux = heightMin y widthAux = ...
       */
      if ($heightAux < $this->heightMin) {
         $heightAux = $this->heightMin;
         // Si no, si el alto es superior al máximo, se recorta, volviendo a adaptar el width.
      } elseif ($heightAux > $this->heightMax) {
         if (($this->heightMax / $heightAux) * $widthAux < $this->widthMin) { // Siempre >= 1
            $widthAux = $this->widthMin;
         } else {
            $widthAux *= $this->heightMax / $heightAux;
         }
         $heightAux = $this->heightMax;
      }
      $this->srcRatio   = $this->srcWidth / $this->srcHeight;
      $this->destRatio  = $widthAux / $heightAux;
      $this->destWidth  = (int) $widthAux;
      $this->destHeight = (int) $heightAux;
      return $this;
   }

   private function setBincolor()
   {
      $hexcolor       = str_split($this->background, 2);
      $bincolor[0]    = hexdec('0x{' . $hexcolor[0] . '}');
      $bincolor[1]    = hexdec('0x{' . $hexcolor[1] . '}');
      $bincolor[2]    = hexdec('0x{' . $hexcolor[2] . '}');
      $this->bincolor = $bincolor;
      return $this;
   }
}

Este es un esquema del proceso:

El rectángulo más grande representa la imagen original, mientras el rectángulo más pequeño que contiene representa el tamaño deseado, cada uno tiene representado su ratio o razón en forma de diagonal. En el método resizeImage() se hace, en primer lugar, una redimensión de la imagen en donde la altura pasa a ser la de la imagen de destino y el ancho se adapta proporcionalmente, la imagen resultante se guarda en la variable de tipo recurso $canvas. A continuación, se procede a recortar, pero no desde la esquina inferior izquierda, sino desde un punto x al que se le suma $this->destWidth, de este modo, se recorta la misma cantidad por el lado izquierdo por el derecho. La razón de esto es que las imágenes tienden a tener el objeto principal en el centro.

El esquema también incluye un ejemplo numérico en el que la imagen original tiene un ratio de 1’3 periódico, es decir, unos 13 píxeles de ancho por cada 10 de alto (no tiene mucho sentido hablar de 1’3 píxeles pues es una unididad indivisible), mientras que el ratio deseado es de 0’665.

Cuando el destino tiene un ratio mayor, podemos ver en el siguiente esquema que la idea es la misma, sólo que en el redimensionamiento inicial lo que se adapta proporcionalmente es el alto a partir de un ancho que se fija:

Esquema reducción imagenEl método initCheck() comprueba, en primer lugar, que exista la imagen original para, a continuación comprobar que el array $imageType tiene todos los parámetros esperados. Finalmente, obtiene el ancho y el alto de la imagen original.

El objetivo del método sizeControl() consiste en calcular el ancho y el alto de la imagen de destino, así como el ratio de esta y la original. Previamente, resuelve incongruencias en los parámetros, como, por ejemplo, que el ancho mínimo tolerado (widthMin) sea menor que el máximo (widthMax).

Veamos a continuación un ejemplo: los tamaños r1, r1_6  y slide que aparecen al principio del presente artículo a partir esta imagen:

Júpiter

Hacer click para obtener la imagen original, aunque WordPress redujo algo su peso.

Tamaño r1_6

Tamaño r1_6. Si hacemos click sobre la imagen podremos apreciar que su tamaño coincide con el especificado y que el peso del fichero se ha reducido mucho, más allá de lo que sería proporcional a la reducción de dimensiones, sin afectar, a simple vista, a la calidad de la imagen.

Tamaño r1

Tamaño r1

El comportamiento es diferente cuando, por un lado existe el parámetro background y, por el otro, height y maxHeight valen lo mismo, así como width y maxWidth, pero la imagen subida no es del mismo ratio. En este caso, aparecerán unas franjas horizontales o verticales en el color de este parámetro, lo cual es útil en circuntancias en que queremos que la imagen que se visualiza en la web sea de un tamaño exacto, como podría ser, por ejemplo, en el caso de slides.

Júpiter slide

Generalmente, es conveniente que el color de las franjas coincida con el color de fondo de la web, aunque en este caso se ha escogido el negro.

Funciones de orden superior

Tanto en matemáticas como en informática, las funciones de orden superior son aquellas que cumplen, al menos, una de estas condiciones:

  1. Esperan como argumento/s una o más funciones.
  2. Devuelven una función como resultado.

Ejemplos en matemáticas son la derivada y la antiderivada o función primitiva.

operador diferencial

El operador diferencial es una función de orden superior

Antiderivada

La antiderivada de una función f es una función F tal que F’ = f

En informática son la esencia de los lenguajes funcionales, pero también aparecen en lenguajes de otros paradigmas. Este es un ejemplo en el lenguaje Scheme en el que la función (f x) recibe un argumento y devuelve una función:

(define (f x)
  (lambda (y) (+ x y)))
(display ((f 3) 7))

Puede ejecutarse aquí para ver el resultado.

Cuando nació Javascript, a algunos programadores les pareció un lenguaje orientado a objetos fallido1, sobretodo porque, por razones comerciales, se le puso un nombre que lo asocia con Java. Desconozco si su creador estuvo muy de acuerdo con ese nombre pues, tal y como se diseño este lenguaje, da bastante juego a la programación funcional. En el siguiente ejemplo, el método filter() es una función de orden superior, pues espera recibir una función como parámetro:

function isPrime(x){
  if (x === 2) {
     return true;
  }
  let test = x%2 !== 0;
  let i = 3;
  stop = Math.floor(Math.sqrt(x)); // Raíz entera de x
  while (test && i <= stop) {
	  test = x%i !== 0;
	  i = i + 2;
  }
  return test;
}

const numbers = [47, 139, 137, 213, 2, 3, 45, 1515];
const primeNumbers = numbers.filter(isPrime);
console.log(primeNumbers);

Lo que este programa hace es filtrar la formación de números naturales “numbers“, dejando sólo los que sean primos en “primeNumbers“. Cada elemento de “numbers” será evaluado por la función “isPrime” mediante la criba de Eratóstenes. El lector puede ejecutarlo accediendo a la consola del navegador pulsando F12 y modificar el valor de “numbers” con los números (o el número) que quiera saber si son primos o no.

Este tipo de funciones están en prácticamente todos los lenguajes modernos, incluso en los que no se tuvo en cuenta el paradigma funcional en el momento de su creación. Es el caso de PHP, donde podemos encontrar una gran cantidad de funciones que esperan otra función, como es el caso de, por ejemplo, preg_replace_callback()2:

$capitalice = function($coincidencia) {
    return strtoupper($coincidencia[1]);
};

echo preg_replace_callback('~-([a-z])~', $capitalice, 'hola-mundo');

Además de usar las implementadas en funciones y métodos propios del lenguaje, también podemos crear las nuestras, de forma parecida a un lenguaje completamente funcional. En Javascript, la Wikipedia nos ofrece el siguiente ejemplo:

const twice = (f, v) => f(f(v));
const add3 = v => v + 3;

console.log(twice(add3, 7));

Lo mismo es posible en PHP:

$twice = function($f, $v) {
    return $f($f($v));
};

$f = function($v) {
    return $v + 3;
};

echo($twice($f, 7));

La programación funcional pretende tratar la programación como la evaluación de funciones matemáticas, paradigma muy diferente a la programación imperativa, basada en estados y en instrucciones que lo cambian. Tal vez las características funcionales que tienen algunos lenguajes puedan ayudarnos a introducirnos en un paradigma, el funcional, que nos exige una forma muy distinta de enfocar los problemas.


 

1 Todavía hoy en día, y a pesar de los cambios que ha sufrido en los últimos ECMA, sigue despertando las críticas de los programadores que, debido a su nombre, esperan que se comporte como un lenguaje completamente orientado a objetos, como Java, y se dan de bruces contra la realidad.

2 El parámetro que recibe la función contenida en $capitalize son las coincidencias que encuentre la expresión regular.

Cómo intercambiar el valor de dos variables enteras sin una intermedia

Cómo intercambiar el valor de dos variables enteras sin una intermedia es, en mi opinión, una mera curiosidad. Ahora bien, parece ser que en algunas entrevistas de trabajo aparece esta pregunta. Cuando se me ha pedido colaborar en el proceso de selección de un futuro compañero, no se me ha ocurrido incluir semejante pregunta en la prueba, pues no creo que esta sirva para apreciar la calidad de un programador. No obstante, si el presente artículo, además de presentar una anécdota, le sirve a alguien a superar una prueba, será un plus.

A continuación, voy a exponer dos métodos que, al usar Javascript para su implementación, el lector podrá ejecutarlos fácilmente presionando F12 para acceder a la consola.

Método matemático

Fácilmente lo podemos deducir con papel y lápiz:

var a = 3;
var b = 2;

a = a + b;
b = a - b; // Con el paso anterior y el actual es b = a + b - b
a = a - b; // a = a + b - a
console.log(a);
console.log(b);

Este método tiene una limitación con números muy grandes en los lenguajes que no manejan por si mismos el desbordamiento de enteros, que no es el caso de Javascript.

Método informático

Este es el mejor, consiste en usar el operador a nivel de bits XOR que, como se explica en el link, devuelve 1 si y solo si x e y son diferentes, mientras que en caso contrario retorna 0.

var x = 10;
var y = 3;

x = x^y;
y = x^y;
x = x^y;

console.log(x);
console.log(y);

Esta es la tabla de la verdad con los valores iniciales 10 y 3 para x e y, respectivamente:
Cómo intercambiar dos variables sin temporalAfirmé que es el mejor debido a que funcionará sin problemas en cualquier lenguaje, la única condición es que disponga de operadores a nivel de bits, algo que prácticamente todos cumplen.

Decimales del número e por Taylor

Tanto Jhon von Neumann como Steve Wozniack hicieron el ejercicio de encontrar miles de decimales del número e, en el ENIAC (uno de los primeros ordenadores de propósito general) y en un Apple II, respectivamente. Si personas de tan elevado tamaño intelectual consideraron oportuno hacerlo, ¿quién soy yo para contrariarlos? Así que, salvando las distancias, me he decidido a hacerlo.

El método empleado han sido las series de Taylor. Para el mismo, se requiere calcular el factorial, la siguiente función recursiva lo calcula para el número recibido como parámetro:

function factorial(a) {
  if (a === 0) {
      return 1;
  }else{
      return a * factorial(a - 1);
  }
}

El siguiente código Javascript calcula, a partir del polinomio de Taylor1 de grado 20 en torno al punto a=0, es decir, e1, unos cuantos decimales:

function factorial(a) {
  if (a === 0) {
      return 1;
  }else{
      return a * factorial(a - 1);
  }
}

var e = 1;

for (i = 1; i <= 19; i++) {
   e = e + 1/factorial(i);
}
console.log(e);

El lector puede copiar el código y ejecutarlo en el mismo navegador con el que está leyendo este artículo, presionando F12 y accediendo a la consola:

Decimales de e en Javascript por Taylor

Decimales de e en Javascript por Taylor. Ejecutado en Firefox.

Por culpa del uso de la coma flotante que hace Javascript (ya incluido en el microprocesador), no podemos sacar más decimales, pues el resultado será redondeado al que vemos en la imagen. Para salvar este inconveniente, en PHP existe la librería BC Math, que nos permite trabajar con números de – casi – cualquier tamaño y precisión. Para que después digan que es un lenguaje de juguete… Éste es el código:

<?php

function factorial($a) {
  if ($a === 0) {
      return 1;
  }else{
      return bcmul($a, factorial($a - 1), 3000);
  }
}

$e = '2';

for ($i = 2; $i <= 1000; $i++) {
   $e = bcadd($e, bcdiv('1', factorial($i), 3000), 3000);
}
echo ($e);

Con el que se obtienen 2572 dígitos correctos:

2.71828182845904523536028747135266249775724709369995957496696762772407663035354759457138217852516642742746639193200305992181741359662904357290033429526059563073813232862794349076323382988075319525101901157383418793070215408914993488416750924476146066808226480016847741185374234544243710753907774499206955170276183860626133138458300075204493382656029760673711320070932870912744374704723069697720931014169283681902551510865746377211125238978442505695369677078544996996794686445490598793163688923009879312773617821542499922957635148220826989519366803318252886939849646510582093923982948879332036250944311730123819706841614039701983767932068328237646480429531180232878250981945581530175671736133206981125099618188159304169035159888851934580727386673858942287922849989208680582574927961048419844436346324496848756023362482704197862320900216099023530436994184914631409343173814364054625315209618369088870701676839642437814059271456354906130310720851038375051011574770417189861068739696552126715468895703503540212340784981933432106817012100562788023519303322474501585390473041995777709350366041699732972508868769664035557071622684471625607988265178713419512466520103059212366771943252786753985589448969709640975459185695638023637016211204774272283648961342251644507818244235294863637214174023889344124796357437026375529444833799801612549227850925778256209262264832627793338656648162772516401910590049164499828931505660472580277863186415519565324425869829469593080191529872117255634754639644791014590409058629849679128740687050489585867174798546677575732056812884592054133405392200011378630094556068816674001698420558040336379537645203040243225661352783695117788386387443966253224985065499588623428189970773327617178392803494650143455889707194258639877275471096295374152111513683506275260232648472870392076431005958411661205452970302364725492966693811513732275364509888903136020572481765851180630364428123149655070475102544650117272115551948668508003685322818315219600373562527944951582841882947876108526398139559900673764829224437528718462457803619298197139914756448826260390338144182326251509748279877799643730899703888677822713836057729788241256119071766394650706330452795466185509666618566470971134447401607046262156807174818778443714369882185596709591025968620023537185887485696522000503117343920732113908032936344797273559552773490717837934216370120500545132638354400018632399149070547977805669785335804896690629511943247309958765523681285904138324116072260299833053537087613893963917795745401613722361878936526053815584158718692553860616477983402543512843961294603529133259

Aquí está el link que ejecuta este código:

https://www.victoriglesias.net/e.php

Al seguir el link, el lector con interés en el tema descubrirá que proporciona más decimales, pero, como dije, sólo los primeros 2572 son correctos. Supongamos que sabemos que e está por debajo de 3, pero no podemos concretar más, el error, por el residuo del teorema de Taylor, sería 3/(i + 1)!, es decir:

+-0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000744805637342692...

Por cierto, no por acotar mejor e, por ejemplo 2’8, dejaríamos de obtener menos posiciones a 0 hasta obtener los primeros decimales del error. 2’8/(i + 1)! es:

+-0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000695151928186513...

Por lo tanto, aunque acotemos con más precisión e, la fórmula del residuo nos sigue asegurando que hemos calculado correctamente la misma cantidad de decimales.

Si parecen insuficientes y se desea mayor precisión, bastará con incrementar el tope de la variable i del bucle a más de 1000. Eso sí, si nos tomamos este cálculo en serio, sería aconsejable optimizar previamente la función recursiva.

En definitiva, aunque me quedé muy lejos del récord mundial ;-), fue entretenido hacerlo y espero que al lector le parezca interesante también o, por lo menos, curioso.


1 Se llama serie de Maclaurin el caso concreto de Taylor a=0

El futuro es de Javascript

El gran ruido de la invasión en el lado servidor de Javascript empezó en el año 2009 con Node.js: no era el primer proyecto que lo intentaba pero sí el primero que lo lograba, y su logro aportó más que poder disponer del mismo lenguaje en el cliente y en el servidor. Por otro lado, sobre las posibilidades que ofrece actualmente Javascript en el navegador gracias a los compiladores JIT, ya he hablado en otro artículo.

Aunque el creador de Node.js, Ryan Dahl, abandonara su proyecto hace ya años, dejando de lado la web para pasar a proyectos más interesantes de Machine Learning, afirmando que Go es una mejor herramienta para servidores web con gran volumen de carga, creo que la inercia que tiene Javascript a día de hoy, le garantiza el dominio en los próximos años.

Desde luego que Javascript dista de ser un gran lenguaje de programación, pero la historia de la informática está plagada de casos en los que no se impuso lo que técnicamente era mejor, pues pesaron más factores relacionados con el mundo de los negocios. En mi opinión, el impulso que está cogiendo Javascript en los últimos años, hará que la palabra de moda en el personal de recursos humanos sea “Javascript”. Si trabajas en la industria deberías tenerlo en cuenta.

Encuesta stackoverflow 2018

Tecnologías más populares en la encuesta de Stackoverflow del presente año.

Por supuesto, los programadores en lenguajes “tradicionales” del lado servidor, como PHP, Java o Python, van a seguir teniendo trabajo los próximos años, aunque sólo sea, en el peor de los casos, por los millones de líneas de código en producción que alguien va a tener que seguir manteniendo; aunque a día de hoy no creo que se llegue a ese extremo; seguro que -todavía- se desarrollarán proyectos nuevos en estos lenguajes.


Editado el 18/4/2018:

Google Brain ha llevado a Javascript su librería de TensorFlow: TensorFlow.js Es decir, llega a los navegadores, y por lo tanto a los ordenadores, móviles y tablets, el machine learning y las redes neuronales.

Cómo comentar el código

En la lista de libros frecuentemente citados y raramente leídos, probablemente encontraríamos desde “El fin de la historia y el último hombre”, de Francis Fukuyama, hasta “Code Complete”, de Steve McConnell. Este último es el que voy a citar aquí, sin haberlo leído para seguir la tradición, concretamente su muy extendida afirmación de que los comentarios no tan solo no son necesarios sino que pueden conseguir el efecto contrario: un código complejo que el programador no trabaja para simplificarlo por tener a su disposición el camino, inicialmente más breve, de los comentarios.

En mi humilde opinión, aunque estoy de acuerdo en gran parte con la opinión de McConnell y otros, los comentarios sí son necesarios. Estos deben describir el porqué o describir qué se está haciendo, no el cómo, pues es el cómo lo único que puede delegarse a un código claro y descriptivo. Cuando el proyecto carece de documentación o no se mantiene actualizada, los comentarios que explican el porqué son aun más necesarios.

A continuación, veamos algunos ejemplos prácticos para ilustrar mi opinión:

class Model_Session
{

   /**
    * Sigue el patrón singleton para encapsular la sesión.
    * No extiende Model_Signia_Abstract porque debe definir construct privado.  
    */
   private $calledClass;

   protected function __construct($options)
   {
      $this->calledClass = explode('_', get_called_class())[1];
   }

   final static function getInstance($options = null)
   {
      static $instances = array();

      $calledClass = get_called_class();

      if (!isset($instances[$calledClass])) {
         $instances[$calledClass] = new $calledClass($options);
      }

      return $instances[$calledClass];
   }

   public function __clone()
   {
      trigger_error("An instance of the class " . __CLASS__ . " already exists.", E_USER_ERROR);
   }

   protected function set($value)
   {
      $_SESSION[$this->calledClass] = $value;
   }

   public function get($value)
   {
      if (isset($_SESSION[$this->calledClass][$value])) {
         return $_SESSION[$this->calledClass][$value];
      } else {
         return false;
      }
   }

   public function isLogged()
   {
      return isset($_SESSION[$this->calledClass]);
   }
   
   public function delete($key)
   {
      unset($_SESSION[$this->calledClass][$key]);
   }
   
   public function logout()
   {
      unset($_SESSION[$this->calledClass]);  
   }
   .
   .
   .
}

Algunos consideran que singleton es un antipatrón de diseño, pero este código está ahora mismo solucionando necesidades reales de empresas y soy de la opinión de que la teoría existe para ayudarnos, no para interferir en aquello por lo que nos pagan. En la misma línea, aunque trigger_error debería evitarse, en los métodos mágicos prefiero lanzar un error a lanzar una excepción. Aclarado esto, regresemos al tema que ocupa este artículo. Con la primera línea del comentario explico por qué y para qué uso el patrón:

Sigue el patrón singleton para encapsular la sesión.

La segunda línea está pensada para responder tanto el porqué que me pueda plantear yo cuando revise el código tiempo después y no recuerde por qué lo programé así, como el que se pueda plantear otro programador que vea esta clase por primera vez:

No extiende Model_Signia_Abstract porque debe definir construct privado.

El resto de la clase no tiene más comentarios y tampoco le hacen falta, pero, ¿qué sucede cuando los nombres de las variables y métodos no son descriptivos? Que nos vemos obligados a crear comentarios que clarifiquen cómo funciona la rutina. Comparemos estos dos métodos:

function permsM($m)
{
      $perms = [];
      
      foreach (['add', 'view', 'edit', 'delete'] as $a) {
         if (isset($this->get('permissions')[$a]) && in_array($m, $this->get('permissions')[$a])) {
            $perms[] = $a;
         }
      }

      return $perms;
}

Probablemente el ejemplo esté un poco forzado, pero sin duda demuestra que, o bien añadimos comentarios por doquier, o bien usamos nombres descriptivos:

public function getPermissionsForModule($sModule)
{
      $aPermissions = [];
      
      foreach (['add', 'view', 'edit', 'delete'] as $action) {
         if (isset($this->get('permissions')[$action]) && in_array($sModule, $this->get('permissions')[$action])) {
            $aPermissions[] = $action;
         }
      }

      return $aPermissions;
}

Cuando tenía unos 9 años hice mi primer “programa”, lo sufrió un Zx Spectrum 128, su cantidad de memoria estaba contenida en su nombre, en Kilobytes. Con semejante restricción, entiendo que en la época tenía lógica optimizar el consumo de memoria incluso recortando el nombre de las funciones y variables, pero a día de hoy eso no tiene ningún sentido. Además, tanto los IDE como sencillos editores (por ejemplo Notepad++), disponen de la funcionalidad de autocompletar texto, que nos ayuda a recordar y escribir correctamente los nombres de variables, funciones, métodos y clases sin importar cuán largos sean. Por lo tanto, a día de hoy no hay excusas para no usar nombres descriptivos.

En este sentido, las constantes ayudan enormemente:

if ($file['error'] === 0 { //¿Mande? ¿Fue bien o mal?

if ($file['error'] === UPLOAD_ERR_OK {

Podemos llegar a ser más papistas que el Papa con el código que se explica a si mismo:

if ($file['size'] > 5242880) { // 5 MB

if ($file['size'] > 5*1024*1024) {

Otros comentarios necesarios

– Cuando, por falta de tiempo, se ha tenido que programar de forma poco óptima, indicarlo ayudará más adelante, cuando el proyecto haya sido entregado, a reconocer las partes que necesitan refactoring más urgentemente.

– Existen expresiones que son complejas y no hay azúcar sintáctico que las simplifique, como por ejemplo las expresiones regulares; en estos casos, comentar qué se está buscando aumentará la productividad de todo el equipo.

– Finalmente, cuando hemos copiado un código con licencia que lo permita, es de caballeros citar el autor y la fuente (si es que no estamos obligados directamente por la misma).

En definitiva, si cada vez que escribimos un comentario nos preguntamos si es realmente necesario o, si por el contrario, está encubriendo un código poco legible, conseguiremos elaborar software más fácil de mantener, tanto para nosotros como para quienes vengan después. Si describe cómo procesa, lo más probable es que nos estemos haciendo trampas.